mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
1313 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23a2c42469 | ||
|
|
7993e3d10d | ||
|
|
bde9ee5a4c | ||
|
|
f9846a27b6 | ||
|
|
ab74afdd8d | ||
|
|
7cb21a24d4 | ||
|
|
5e26f6f3ae | ||
|
|
cce32f8dc5 | ||
|
|
1505055334 | ||
|
|
027542e2e5 | ||
|
|
0294fba042 | ||
|
|
07699b5871 | ||
|
|
b8849c8cbf | ||
|
|
00ab5cd6f2 | ||
|
|
858e87ab0d | ||
|
|
6c485c282d | ||
|
|
4bae6851d1 | ||
|
|
5288a7dc9a | ||
|
|
516deb22aa | ||
|
|
4e2ffe79a4 | ||
|
|
47256cdde6 | ||
|
|
48ee9ddb22 | ||
|
|
ad13f14432 | ||
|
|
4e34e8f1c2 | ||
|
|
cb76945688 | ||
|
|
87538711b6 | ||
|
|
822b15ea43 | ||
|
|
3598c11c8d | ||
|
|
d45325b6d7 | ||
|
|
64fc859dac | ||
|
|
3536fd7d60 | ||
|
|
15099fade5 | ||
|
|
6fe5899639 | ||
|
|
4961a4fab1 | ||
|
|
e549aac127 | ||
|
|
2bca242fdc | ||
|
|
4bd0ab76c6 | ||
|
|
a46e5ef621 | ||
|
|
ae8ad55cb8 | ||
|
|
84b1c9d8c2 | ||
|
|
fd40a80a68 | ||
|
|
5f19c55731 | ||
|
|
610c2ea131 | ||
|
|
8f8c484bc6 | ||
|
|
f1c7f8e813 | ||
|
|
e377d33652 | ||
|
|
db9ce032a4 | ||
|
|
dfdda2c871 | ||
|
|
32090aee16 | ||
|
|
20326054da | ||
|
|
dc60eee50e | ||
|
|
cf66532ac1 | ||
|
|
217950b9ad | ||
|
|
f3ee8d6322 | ||
|
|
b2aeaa2dcc | ||
|
|
dcb99e4972 | ||
|
|
25fd4d9f2c | ||
|
|
bf7940d7ff | ||
|
|
19977b4659 | ||
|
|
1a9551db82 | ||
|
|
5b46ce579b | ||
|
|
493055731e | ||
|
|
415ddf59bb | ||
|
|
03dc63f6c8 | ||
|
|
4eada9a908 | ||
|
|
512993b57f | ||
|
|
ca91bb2f7f | ||
|
|
8993affdc0 | ||
|
|
ff23e5ba37 | ||
|
|
0d1221155e | ||
|
|
c5eabe3143 | ||
|
|
97c7c34f6f | ||
|
|
3e54d70ae2 | ||
|
|
a7f470d1d9 | ||
|
|
428581dd05 | ||
|
|
572a1ca42a | ||
|
|
3bfc3dd45b | ||
|
|
db7e8b5619 | ||
|
|
54c438d8d3 | ||
|
|
1731af3f29 | ||
|
|
11fd81e398 | ||
|
|
88dfa7baa6 | ||
|
|
75e95c45a2 | ||
|
|
c6ea29d916 | ||
|
|
e9f587ecba | ||
|
|
3553101eb3 | ||
|
|
b01dd76be1 | ||
|
|
95614e5220 | ||
|
|
ae9c2ab165 | ||
|
|
33d328d967 | ||
|
|
759db7d7d5 | ||
|
|
4c18e08036 | ||
|
|
a5b88c489e | ||
|
|
17f977a9de | ||
|
|
c571dd4f0e | ||
|
|
94ed41f236 | ||
|
|
26fc878944 | ||
|
|
b57e9f58fd | ||
|
|
d18fc97717 | ||
|
|
b80d1925ff | ||
|
|
31a049eb69 | ||
|
|
cf45e57d9c | ||
|
|
1b91c26409 | ||
|
|
5d273a0c76 | ||
|
|
da6df07a9d | ||
|
|
7799e14121 | ||
|
|
2eaf689f71 | ||
|
|
8c45c8b8b9 | ||
|
|
1d3ef8734c | ||
|
|
547adda446 | ||
|
|
fbf8003237 | ||
|
|
4d922a0f9b | ||
|
|
8413c38295 | ||
|
|
adf582dba7 | ||
|
|
921d95357d | ||
|
|
1f70929e53 | ||
|
|
a7ddcc9c0f | ||
|
|
cb4b6c844a | ||
|
|
8c2b5ea7c4 | ||
|
|
de1ec90133 | ||
|
|
44a24605ad | ||
|
|
570db98548 | ||
|
|
d22d9b22b1 | ||
|
|
b93804529d | ||
|
|
78bf5648e7 | ||
|
|
c3278a8262 | ||
|
|
d4f6d65e1d | ||
|
|
5ebd004a10 | ||
|
|
459863bcff | ||
|
|
fe3401e037 | ||
|
|
933ce76057 | ||
|
|
d5a42e9d9c | ||
|
|
b8eca1ffbf | ||
|
|
49a1b4262d | ||
|
|
e903c941cb | ||
|
|
974206ebe1 | ||
|
|
687662c990 | ||
|
|
d1df3cd4d5 | ||
|
|
656bf2c60c | ||
|
|
633137d501 | ||
|
|
3916e23bbd | ||
|
|
afd2e214bc | ||
|
|
8d8a133c89 | ||
|
|
d085807070 | ||
|
|
58ddff0881 | ||
|
|
bfe20c11c3 | ||
|
|
e7c6d2c9d9 | ||
|
|
cdb8d746ef | ||
|
|
cadcc6cabe | ||
|
|
11da8d0dff | ||
|
|
f842bca471 | ||
|
|
0a699df5e8 | ||
|
|
5180285456 | ||
|
|
8ce69e802d | ||
|
|
0046df4b51 | ||
|
|
c2609b239f | ||
|
|
28408a9f64 | ||
|
|
9950ce2334 | ||
|
|
2b64c573c3 | ||
|
|
f4a3b194da | ||
|
|
f04b3d5042 | ||
|
|
59cf6f5ec9 | ||
|
|
3d3f692fd8 | ||
|
|
b2596c660b | ||
|
|
e715741abc | ||
|
|
813125e122 | ||
|
|
92ea45070c | ||
|
|
9412110c82 | ||
|
|
960b28c90a | ||
|
|
ca386a4b25 | ||
|
|
99c445a6d6 | ||
|
|
96cd467cfa | ||
|
|
e24d5cb97d | ||
|
|
58c0ef90c9 | ||
|
|
e632fcd933 | ||
|
|
78ff63a9c7 | ||
|
|
e7ccd26c70 | ||
|
|
3db0efa69f | ||
|
|
6fea478d2e | ||
|
|
2c400363e8 | ||
|
|
9d0efedaee | ||
|
|
33e9e0fb2d | ||
|
|
ef1eb4c888 | ||
|
|
0ac2dc388e | ||
|
|
a0bc0fdf21 | ||
|
|
192fce51d7 | ||
|
|
774cff3c72 | ||
|
|
0c59bc5e35 | ||
|
|
64bc36304f | ||
|
|
7e1779d48c | ||
|
|
b6c48a694b | ||
|
|
216d5f6b52 | ||
|
|
bebca337c4 | ||
|
|
61ecb13bf0 | ||
|
|
37900a92db | ||
|
|
997ed151db | ||
|
|
3db2c0d43e | ||
|
|
a8ceeec0fd | ||
|
|
83a1cce1ea | ||
|
|
548ace0115 | ||
|
|
092979b8cc | ||
|
|
02ebb9f0c3 | ||
|
|
5ff0bfb81d | ||
|
|
ed8b7d400c | ||
|
|
2cdff00788 | ||
|
|
339c11dd86 | ||
|
|
0292d991af | ||
|
|
bf944d9219 | ||
|
|
7df8c8c287 | ||
|
|
217c082ac1 | ||
|
|
588dcf492b | ||
|
|
2fdf939ca9 | ||
|
|
5f38625f21 | ||
|
|
d669eb6d05 | ||
|
|
e9d5a91def | ||
|
|
b765dc005b | ||
|
|
303b455965 | ||
|
|
f45a6a7004 | ||
|
|
f987393b32 | ||
|
|
c23afed39a | ||
|
|
1fd8139138 | ||
|
|
269f80bf8e | ||
|
|
0b51d970b4 | ||
|
|
a8e565eca8 | ||
|
|
50c8e3fcda | ||
|
|
ec824927c1 | ||
|
|
4ebdb19682 | ||
|
|
3cd9c02f71 | ||
|
|
e2cebe26e8 | ||
|
|
c174d19d1e | ||
|
|
cdc1b5d629 | ||
|
|
b01159f234 | ||
|
|
5d439b127b | ||
|
|
c46088405a | ||
|
|
003668cfaa | ||
|
|
6447db063a | ||
|
|
65f846ade0 | ||
|
|
407d8a5019 | ||
|
|
6cb6cb9e69 | ||
|
|
1c06806f90 | ||
|
|
7d15452c30 | ||
|
|
07286a73b1 | ||
|
|
02c3b1c9e2 | ||
|
|
d2fb2b8095 | ||
|
|
328dab2463 | ||
|
|
97a096b507 | ||
|
|
3b4dec442d | ||
|
|
16a0815fac | ||
|
|
3cb678f84c | ||
|
|
49948d72f3 | ||
|
|
8b0e96474b | ||
|
|
bf6b72eb55 | ||
|
|
8421cabb9d | ||
|
|
46de65cab9 | ||
|
|
351c64e99e | ||
|
|
1a62f1299d | ||
|
|
4b256cab31 | ||
|
|
233969bb58 | ||
|
|
c6766d45b5 | ||
|
|
4317c8e583 | ||
|
|
e3c3f5a6d0 | ||
|
|
d4c20c472b | ||
|
|
b77cce4ec5 | ||
|
|
8bcd36377a | ||
|
|
c9c2e39531 | ||
|
|
dd8af5565b | ||
|
|
a92092340b | ||
|
|
c5eec32c58 | ||
|
|
7465250141 | ||
|
|
69c396825b | ||
|
|
6aba43f6cc | ||
|
|
988a8526b5 | ||
|
|
3791b75000 | ||
|
|
2fcce3b3c5 | ||
|
|
da80ebcc6b | ||
|
|
cc44ecc62f | ||
|
|
0881a8ae6f | ||
|
|
d3a02ec038 | ||
|
|
42081b1937 | ||
|
|
9f6d1b10ad | ||
|
|
1616df2f61 | ||
|
|
c670ce416b | ||
|
|
f48fce8bd3 | ||
|
|
24e2da4557 | ||
|
|
416ab4ebf0 | ||
|
|
a2aafeb959 | ||
|
|
34c4614682 | ||
|
|
9e429239ab | ||
|
|
96c001e668 | ||
|
|
4facbe02fb | ||
|
|
a70765ed90 | ||
|
|
4a5e95511e | ||
|
|
dfb3d21a6d | ||
|
|
b0554682ed | ||
|
|
da4a09f977 | ||
|
|
3068210a93 | ||
|
|
7f4c7fe4e8 | ||
|
|
dd3711bdbd | ||
|
|
b15e8d5bbc | ||
|
|
dca3ba2f77 | ||
|
|
24305ba5bf | ||
|
|
4e52f9699b | ||
|
|
89ba802b23 | ||
|
|
020fc15d98 | ||
|
|
1273023ac3 | ||
|
|
4a73c366fa | ||
|
|
a5a4ef3fd7 | ||
|
|
2a49f177fe | ||
|
|
8918422156 | ||
|
|
fc7b2b11a2 | ||
|
|
402d080990 | ||
|
|
ae48e75ad7 | ||
|
|
440cbd5235 | ||
|
|
d7412c4df1 | ||
|
|
aa76bf39ab | ||
|
|
29b54d6638 | ||
|
|
f7cf978f68 | ||
|
|
1ac1cd6c14 | ||
|
|
5949571fe7 | ||
|
|
1c86ec5b8d | ||
|
|
43e7ad1b1c | ||
|
|
2438b8b66b | ||
|
|
68698e0ac8 | ||
|
|
efb0f6e23b | ||
|
|
4b3f743885 | ||
|
|
bab2846513 | ||
|
|
af83bf6712 | ||
|
|
fe6832fae8 | ||
|
|
2221a13a4d | ||
|
|
f3dbcdc7b3 | ||
|
|
af7ae048f8 | ||
|
|
1071d063ab | ||
|
|
7614d8f87a | ||
|
|
f4e50079de | ||
|
|
92e2ff4985 | ||
|
|
9b1ca64a75 | ||
|
|
ad6eacb3e9 | ||
|
|
fd535183ee | ||
|
|
6bc1dc4020 | ||
|
|
d59aa6af25 | ||
|
|
f139c02e95 | ||
|
|
7249785bcb | ||
|
|
0a8b026ccf | ||
|
|
82a6b83524 | ||
|
|
9024a19658 | ||
|
|
53da1099d1 | ||
|
|
395bb64b26 | ||
|
|
7a07263281 | ||
|
|
1c6825cc7a | ||
|
|
5ab9929cbb | ||
|
|
36d730229a | ||
|
|
b63691f6e2 | ||
|
|
13fad06239 | ||
|
|
f21960ec9d | ||
|
|
ecabff7eb4 | ||
|
|
80b2710e6f | ||
|
|
b0f0b7b75e | ||
|
|
fb3a01fa3a | ||
|
|
d30d79b5be | ||
|
|
ea80b9208d | ||
|
|
394f77c3ff | ||
|
|
2f39dc19a2 | ||
|
|
2aa79f4fbe | ||
|
|
bfa36a72b9 | ||
|
|
71ef8f0636 | ||
|
|
20cf0b7aeb | ||
|
|
946d02536b | ||
|
|
ac2a177070 | ||
|
|
21fe249d62 | ||
|
|
d84f5b30b8 | ||
|
|
188de756be | ||
|
|
baf472f83f | ||
|
|
841df4da71 | ||
|
|
f2de2d644a | ||
|
|
d9a9e9eb30 | ||
|
|
4a1597f295 | ||
|
|
86d3180666 | ||
|
|
864de6a7a4 | ||
|
|
ea6bec96d3 | ||
|
|
f618f99ece | ||
|
|
12ce441e67 | ||
|
|
0985bfb775 | ||
|
|
9de9661baa | ||
|
|
6f3f631fd1 | ||
|
|
da511334d2 | ||
|
|
40342af459 | ||
|
|
8e8bbb00f5 | ||
|
|
ef9c4476a0 | ||
|
|
d5aa965522 | ||
|
|
7a756e5d9d | ||
|
|
7c06399512 | ||
|
|
7d709542ca | ||
|
|
fa955cc2a4 | ||
|
|
aa80900a8e | ||
|
|
b4b492824e | ||
|
|
b29517bd01 | ||
|
|
0f192579ac | ||
|
|
beae9acfcc | ||
|
|
53216a500d | ||
|
|
e7858b6d7e | ||
|
|
0d278f5da8 | ||
|
|
b1ee6fd7ed | ||
|
|
d6bcffa929 | ||
|
|
c5a25f610a | ||
|
|
194e1e9151 | ||
|
|
c2f2e26ec5 | ||
|
|
6d4617960d | ||
|
|
70137409ed | ||
|
|
ed241ba032 | ||
|
|
2a44558fbd | ||
|
|
a10c2ec88d | ||
|
|
2d1dfb3b34 | ||
|
|
da1dda3e1d | ||
|
|
967ce43b59 | ||
|
|
8e358ef35a | ||
|
|
51b81b472d | ||
|
|
4f6acf114c | ||
|
|
7c7d9d6326 | ||
|
|
4841b6d4ba | ||
|
|
fc121f9785 | ||
|
|
332b2869ef | ||
|
|
c372929ab6 | ||
|
|
f4e64ac253 | ||
|
|
da87990bd6 | ||
|
|
cf1feee21d | ||
|
|
ad9226eeec | ||
|
|
6603e39e6a | ||
|
|
5e2236f9ff | ||
|
|
acb2d171e8 | ||
|
|
f3bb3943c9 | ||
|
|
7bd604e3be | ||
|
|
d56e389a95 | ||
|
|
bb4a20174c | ||
|
|
15be181642 | ||
|
|
db2e350e29 | ||
|
|
1342bcedaf | ||
|
|
be6d41ffe5 | ||
|
|
53f69bf089 | ||
|
|
51edfeb3d0 | ||
|
|
9e57ed2b1f | ||
|
|
4ae0844ee3 | ||
|
|
06a5a40e90 | ||
|
|
f0382357ca | ||
|
|
4be99c2989 | ||
|
|
9c0826592c | ||
|
|
8f0997d17d | ||
|
|
58b1a891ce | ||
|
|
e9abbe89f3 | ||
|
|
b3e6cd59a1 | ||
|
|
f22d023c4b | ||
|
|
4c8111ef98 | ||
|
|
f05dce54a7 | ||
|
|
514e0fd4b6 | ||
|
|
cb939ed450 | ||
|
|
7ea38a0c9d | ||
|
|
f1ddbfaae4 | ||
|
|
449739e6a3 | ||
|
|
ac9345b47a | ||
|
|
cd198dfea8 | ||
|
|
3187b5ba2d | ||
|
|
eea3a29699 | ||
|
|
5356044b77 | ||
|
|
71e6a94af7 | ||
|
|
5662be894e | ||
|
|
a065becea5 | ||
|
|
bf8cdda2f5 | ||
|
|
8afbece683 | ||
|
|
b3b1961496 | ||
|
|
5ffe5ab43f | ||
|
|
dc3c2823ac | ||
|
|
82c5820767 | ||
|
|
f5cf7ac25b | ||
|
|
456017e0ae | ||
|
|
c5cec1cc77 | ||
|
|
4d1a7624f4 | ||
|
|
f71627567b | ||
|
|
c8f996e29f | ||
|
|
be2a9a8d1a | ||
|
|
bb04447c44 | ||
|
|
1116f5330e | ||
|
|
66104da10c | ||
|
|
1c445f88f6 | ||
|
|
e7bc1291a0 | ||
|
|
79bd6e77b8 | ||
|
|
da19fd0d1a | ||
|
|
07890b43ca | ||
|
|
27d0c1ecc2 | ||
|
|
80472ac198 | ||
|
|
f4667f86af | ||
|
|
5fefc12d1e | ||
|
|
13b560971e | ||
|
|
9aed791fc3 | ||
|
|
3dac27a8a9 | ||
|
|
f74e850b5c | ||
|
|
4fe5dfa74c | ||
|
|
636a0dbde7 | ||
|
|
c18a6433d4 | ||
|
|
34034af1c9 | ||
|
|
07639c79d9 | ||
|
|
25d80f35f1 | ||
|
|
75e517a2da | ||
|
|
6684855767 | ||
|
|
10ef8e6e4b | ||
|
|
cecda27d73 | ||
|
|
984e207b59 | ||
|
|
693d0b8f45 | ||
|
|
66df7f1aaf | ||
|
|
259b5e8451 | ||
|
|
e1170d4edb | ||
|
|
81b956c70d | ||
|
|
868eb478d8 | ||
|
|
3db09c4d15 | ||
|
|
83c53113af | ||
|
|
d224358e21 | ||
|
|
72aef114ab | ||
|
|
6045bd89fb | ||
|
|
5b096cc3db | ||
|
|
917af4705b | ||
|
|
9ac53ef8cf | ||
|
|
2fc00508fb | ||
|
|
c72074b48e | ||
|
|
3ef2c946d5 | ||
|
|
aaf1d499bf | ||
|
|
94982392be | ||
|
|
51276c60bf | ||
|
|
78a3f43d9d | ||
|
|
02a44664b9 | ||
|
|
1fa0454288 | ||
|
|
ca0e8dedfb | ||
|
|
ba11afafb9 | ||
|
|
7e1437c6b1 | ||
|
|
1aa5cc9178 | ||
|
|
bc1d685a8c | ||
|
|
f6b9853ad0 | ||
|
|
de38f54f22 | ||
|
|
96213f69a2 | ||
|
|
036333412d | ||
|
|
82e278029c | ||
|
|
b9cdc443d7 | ||
|
|
1561ef56ed | ||
|
|
f368ad946e | ||
|
|
918e71adb7 | ||
|
|
cf3188352b | ||
|
|
6860a18c12 | ||
|
|
ff553cc9dd | ||
|
|
574377636e | ||
|
|
b2d41b1cd9 | ||
|
|
9435830351 | ||
|
|
d694619a95 | ||
|
|
4f11518934 | ||
|
|
2d55d43d40 | ||
|
|
45f7677bdc | ||
|
|
099083ea6b | ||
|
|
7a322b6326 | ||
|
|
d1adb19b8a | ||
|
|
a0b1b34c71 | ||
|
|
bf8b9b90cd | ||
|
|
c5757a0266 | ||
|
|
ee447abcad | ||
|
|
a940a87ddc | ||
|
|
5813e81dc6 | ||
|
|
a6d3be4dbf | ||
|
|
166bec0c08 | ||
|
|
c8d67beb9c | ||
|
|
392dc8af59 | ||
|
|
9605593d11 | ||
|
|
b95a178584 | ||
|
|
fbf6320614 | ||
|
|
e06adc6d7e | ||
|
|
1f76377a7c | ||
|
|
dca75a08ba | ||
|
|
2d61dbc774 | ||
|
|
3ee9a67aa4 | ||
|
|
ae953b0884 | ||
|
|
d5bf210998 | ||
|
|
389285585d | ||
|
|
3656eb4740 | ||
|
|
f1bdf40dda | ||
|
|
d96cb61f26 | ||
|
|
7151615260 | ||
|
|
1550ab9e2f | ||
|
|
1132663cc7 | ||
|
|
3ccb17ce59 | ||
|
|
472ef19100 | ||
|
|
c65306f877 | ||
|
|
f7d80930f2 | ||
|
|
0fdf308874 | ||
|
|
7a8307fe7c | ||
|
|
697f6714a4 | ||
|
|
ec5fb77a66 | ||
|
|
f1c9ab4e4f | ||
|
|
3b0fb6aae8 | ||
|
|
6e72ee62ae | ||
|
|
37bfe44046 | ||
|
|
48ea055781 | ||
|
|
dcadfbbd4a | ||
|
|
9bcedf224e | ||
|
|
69ddec6589 | ||
|
|
72e80dbe0e | ||
|
|
c818aa13eb | ||
|
|
ba87eb6753 | ||
|
|
d170fbdb9f | ||
|
|
c58eb0d5a3 | ||
|
|
59f2bef187 | ||
|
|
1ca51c8586 | ||
|
|
c0936b103c | ||
|
|
9d3246ed12 | ||
|
|
ef99a5d972 | ||
|
|
a31bf77776 | ||
|
|
24e4c48468 | ||
|
|
2721f5ccc9 | ||
|
|
6806caffc7 | ||
|
|
52ca867670 | ||
|
|
72eb360f2d | ||
|
|
2b4736afcd | ||
|
|
7dc7c53029 | ||
|
|
327dcc98e3 | ||
|
|
87deaf1658 | ||
|
|
7679ee7321 | ||
|
|
4553651138 | ||
|
|
5383ba5587 | ||
|
|
432e8ef2bc | ||
|
|
70899d3ab2 | ||
|
|
b42b0d3fe5 | ||
|
|
7d9a84a445 | ||
|
|
1e6c5b205c | ||
|
|
c7620cca6f | ||
|
|
b02bb18a70 | ||
|
|
4e79b09dd9 | ||
|
|
6f5970a2e1 | ||
|
|
3d2cca6762 | ||
|
|
4354590a69 | ||
|
|
ef5b39c410 | ||
|
|
7b8e24a588 | ||
|
|
53841642a8 | ||
|
|
b08112f936 | ||
|
|
53ae5bce13 | ||
|
|
e8e80fe6b5 | ||
|
|
0e848d73f9 | ||
|
|
cbea225d97 | ||
|
|
a7d53227de | ||
|
|
437969eac9 | ||
|
|
bf4b224fcf | ||
|
|
e3117a2a23 | ||
|
|
c6a8e7d9b9 | ||
|
|
c96ab4fcbb | ||
|
|
efea61dc50 | ||
|
|
bc250a6afa | ||
|
|
284fac379c | ||
|
|
5aa13b9084 | ||
|
|
14ed6799d7 | ||
|
|
a7420ff2b5 | ||
|
|
e4e8ad6780 | ||
|
|
c0673c50e6 | ||
|
|
7d94913efb | ||
|
|
c9f73bd325 | ||
|
|
c03176af59 | ||
|
|
2771efb51c | ||
|
|
932b376b4e | ||
|
|
0c4ae63ad5 | ||
|
|
b99f6eb904 | ||
|
|
78af6bbb98 | ||
|
|
537c7e1137 | ||
|
|
5f16439752 | ||
|
|
3a8a94448a | ||
|
|
e9c88ae4f4 | ||
|
|
4847045259 | ||
|
|
997a016122 | ||
|
|
512f2cc9c4 | ||
|
|
6876b1a25b | ||
|
|
107e7d5d91 | ||
|
|
09d79b0a9b | ||
|
|
b5c9d99424 | ||
|
|
176e3fd141 | ||
|
|
95acf63ea3 | ||
|
|
90f5eb1270 | ||
|
|
7dfcba1649 | ||
|
|
e3152188ef | ||
|
|
231afe464a | ||
|
|
e68dc04900 | ||
|
|
4696622b0a | ||
|
|
83ea3c96ec | ||
|
|
333e63156e | ||
|
|
a0c3da17b4 | ||
|
|
4c7a1abd39 | ||
|
|
9fda37158a | ||
|
|
648fd2a622 | ||
|
|
99b0c9900e | ||
|
|
f6258221c1 | ||
|
|
68e534777c | ||
|
|
29686f63ac | ||
|
|
dcc1965bfe | ||
|
|
03ac0c91ae | ||
|
|
709b8ac2b7 | ||
|
|
e9670fd144 | ||
|
|
f9688d7519 | ||
|
|
da8b5a5367 | ||
|
|
28bcd01e8d | ||
|
|
fba67ef951 | ||
|
|
3fa01be9e4 | ||
|
|
270825ab2a | ||
|
|
008515c844 | ||
|
|
301ef1bdc6 | ||
|
|
cf1e167034 | ||
|
|
beed1ba089 | ||
|
|
2ab7e23790 | ||
|
|
fceb5f7b22 | ||
|
|
0dac2f7a8d | ||
|
|
6a6a718898 | ||
|
|
faec6f7f31 | ||
|
|
26dda48e50 | ||
|
|
3108accdee | ||
|
|
e0f060d89b | ||
|
|
380852b58e | ||
|
|
3dea0d2806 | ||
|
|
0505014152 | ||
|
|
3bd8cbc62f | ||
|
|
d583aaa0c3 | ||
|
|
3a7375f15e | ||
|
|
79a5fb469b | ||
|
|
9fd0c74e90 | ||
|
|
335e5d131c | ||
|
|
b7d42c1e93 | ||
|
|
0db0528e8e | ||
|
|
704e7e9f44 | ||
|
|
c58f7f293d | ||
|
|
19095552aa | ||
|
|
a64ff63a41 | ||
|
|
17db2b27bf | ||
|
|
ac8d73b258 | ||
|
|
a6f5c88b47 | ||
|
|
1c0408de08 | ||
|
|
9cebfd9d90 | ||
|
|
e932e5237e | ||
|
|
fbf221ae6d | ||
|
|
32808e4111 | ||
|
|
9f94f9de48 | ||
|
|
4571cf7baa | ||
|
|
bfae582fa3 | ||
|
|
575852e6b5 | ||
|
|
10b4291b54 | ||
|
|
bcf5121937 | ||
|
|
aeaeceb92c | ||
|
|
16f55d4275 | ||
|
|
b588ce920d | ||
|
|
1fb2c831e8 | ||
|
|
246f5d2e20 | ||
|
|
c707b7d128 | ||
|
|
ba41ca45fa | ||
|
|
7aacd6834a | ||
|
|
de14853237 | ||
|
|
9973298e2a | ||
|
|
65c37cc852 | ||
|
|
b6818fd4d2 | ||
|
|
fe7af80198 | ||
|
|
9aed6a06cf | ||
|
|
b3a0961c6c | ||
|
|
d9a9a47075 | ||
|
|
f9bb000ccf | ||
|
|
d6c0cff3bd | ||
|
|
95e171e19a | ||
|
|
d7b206cc93 | ||
|
|
06dfbdf7c8 | ||
|
|
3395a3305f | ||
|
|
5aaa3c09c1 | ||
|
|
b36a0c71d1 | ||
|
|
a402e0c5e6 | ||
|
|
660364d6a7 | ||
|
|
b170fe921e | ||
|
|
84372cef4a | ||
|
|
890178cf25 | ||
|
|
a284de73e6 | ||
|
|
45592ccdfd | ||
|
|
f4094c5eb3 | ||
|
|
dd2b933a0d | ||
|
|
c099b36af3 | ||
|
|
cc83b06cd1 | ||
|
|
5f30a69a9e | ||
|
|
faee41c303 | ||
|
|
e32cfed1d8 | ||
|
|
1e4b971f95 | ||
|
|
b0483cd47d | ||
|
|
40d2f38abe | ||
|
|
59516a8bb1 | ||
|
|
8aa4b7bf7f | ||
|
|
e639a3516d | ||
|
|
0897a09f49 | ||
|
|
6ac0b4ade8 | ||
|
|
34d7896b06 | ||
|
|
688c37ebf4 | ||
|
|
2c00e1ecd9 | ||
|
|
42f5b0a6b8 | ||
|
|
14bc4ed59f | ||
|
|
0b8a3bc3b9 | ||
|
|
c04caff55c | ||
|
|
7f23425e59 | ||
|
|
1aaa429081 | ||
|
|
04fbda46dd | ||
|
|
d821755b49 | ||
|
|
ae7dfeb5b6 | ||
|
|
b0406b9ead | ||
|
|
5bd9369a62 | ||
|
|
285ecaacd0 | ||
|
|
34878bc26a | ||
|
|
76217890c0 | ||
|
|
bf6fa6dd3d | ||
|
|
a9da2ec895 | ||
|
|
f3d3441d02 | ||
|
|
3292f70071 | ||
|
|
49b5dd56b5 | ||
|
|
32acb7e903 | ||
|
|
276b9f1839 | ||
|
|
7a77aabb4b | ||
|
|
aeb69c0f8c | ||
|
|
d9f3f322c5 | ||
|
|
33c4dd4c2d | ||
|
|
ca8349a897 | ||
|
|
cd62ee3f29 | ||
|
|
958b52596c | ||
|
|
c7bcd87f37 | ||
|
|
80852d1135 | ||
|
|
84326e2491 | ||
|
|
e3aec9bc81 | ||
|
|
21b45d2a5b | ||
|
|
842898df15 | ||
|
|
afb7f173cf | ||
|
|
14975ce5bc | ||
|
|
667e747ed1 | ||
|
|
1c51c8ab7d | ||
|
|
39e3fc69e5 | ||
|
|
b42fe05c51 | ||
|
|
ca1ae7cf9b | ||
|
|
2026942b05 | ||
|
|
aa525e4a63 | ||
|
|
3ed39ad20e | ||
|
|
cc2cee4af6 | ||
|
|
6c81752e46 | ||
|
|
a87eac4308 | ||
|
|
a2cd942a95 | ||
|
|
a840ff8f3f | ||
|
|
1c20249884 | ||
|
|
e53d77b501 | ||
|
|
09a59ce2d3 | ||
|
|
8b28f7d14e | ||
|
|
a81ec21762 | ||
|
|
9819b3619e | ||
|
|
311dc61803 | ||
|
|
d934328904 | ||
|
|
6ea20f3503 | ||
|
|
8b3ce85183 | ||
|
|
a059ca6915 | ||
|
|
1e05e30472 | ||
|
|
249e8f2277 | ||
|
|
aaf9ab68c6 | ||
|
|
3d6aee079e | ||
|
|
81d061e74e | ||
|
|
fb93a4a9e3 | ||
|
|
ceec607e7f | ||
|
|
fb082cf50f | ||
|
|
493b1e6d3c | ||
|
|
806c49a690 | ||
|
|
aa347b52ba | ||
|
|
4385eadc28 | ||
|
|
6b20fef52a | ||
|
|
c92740e8a9 | ||
|
|
d13d0bba51 | ||
|
|
d83202b938 | ||
|
|
cc049851d0 | ||
|
|
14a9652324 | ||
|
|
af44e9556d | ||
|
|
7e7eb0efc1 | ||
|
|
8dcb6f24b5 | ||
|
|
79fe6083eb | ||
|
|
dd1a9100c5 | ||
|
|
44998ca450 | ||
|
|
7a153b5c94 | ||
|
|
5a06f5c5fc | ||
|
|
dc7f39677f | ||
|
|
08f5c48fc8 | ||
|
|
9774949cc9 | ||
|
|
53d0f69dc3 | ||
|
|
6081f4947e | ||
|
|
6d18b52931 | ||
|
|
81ecaf945d | ||
|
|
55397f6347 | ||
|
|
2faffc52ee | ||
|
|
6c1f0055dc | ||
|
|
811716592c | ||
|
|
e2d2d63bcd | ||
|
|
dde7ec8e64 | ||
|
|
ce55a8cc4b | ||
|
|
30bfa911fc | ||
|
|
da3f842b8c | ||
|
|
130cbdd7af | ||
|
|
b099634ba1 | ||
|
|
c2afc6cd0a | ||
|
|
80b5470663 | ||
|
|
7411794fa1 | ||
|
|
55fe0d8adc | ||
|
|
b63dd9506e | ||
|
|
6f256e6380 | ||
|
|
2bd4346075 | ||
|
|
f23e5b17b6 | ||
|
|
56a358481e | ||
|
|
d5704cf2a3 | ||
|
|
550e8f32ac | ||
|
|
f90ce04a83 | ||
|
|
ccfb42e4ff | ||
|
|
25e96f82db | ||
|
|
253c327252 | ||
|
|
a75f8686ba | ||
|
|
1ef51e7939 | ||
|
|
746ed57c0e | ||
|
|
5132fcdb8b | ||
|
|
332986ba43 | ||
|
|
472b4fe48c | ||
|
|
fd2d3fcfd7 | ||
|
|
967ac65586 | ||
|
|
16b40cbede | ||
|
|
75890d7bdd | ||
|
|
e8f19b4c0d | ||
|
|
6bdb23449a | ||
|
|
f64cc237fc | ||
|
|
ef2111099a | ||
|
|
df50a6823f | ||
|
|
324020d5fe | ||
|
|
544691ab05 | ||
|
|
5236de5b03 | ||
|
|
91b370650a | ||
|
|
e062f2dfa8 | ||
|
|
c1a25756c2 | ||
|
|
d692994ea4 | ||
|
|
a3590dfa26 | ||
|
|
da9b7b0368 | ||
|
|
054fad5360 | ||
|
|
e0954f3b36 | ||
|
|
76fe7d4eba | ||
|
|
942d8412c4 | ||
|
|
2eaa199e6a | ||
|
|
83ce57302d | ||
|
|
de727f854a | ||
|
|
0627366b2f | ||
|
|
586e0df62d | ||
|
|
c0577ea87a | ||
|
|
d81e7dc00e | ||
|
|
9a5f224931 | ||
|
|
21d6ce2380 | ||
|
|
972f664b6b | ||
|
|
1dc4ad1efa | ||
|
|
a0a609e8af | ||
|
|
dc1f202eca | ||
|
|
ce5cd2202f | ||
|
|
2df5cb114d | ||
|
|
ef0304beff | ||
|
|
dd2ae64120 | ||
|
|
cde6bdfa77 | ||
|
|
f397b2264c | ||
|
|
768ff1a850 | ||
|
|
7735aad9d6 | ||
|
|
7bff9b6269 | ||
|
|
24f0bb4af5 | ||
|
|
c3f9d8e41b | ||
|
|
64b6f09b0d | ||
|
|
a73104b566 | ||
|
|
41907209bb | ||
|
|
d12feed623 | ||
|
|
9e0c3e7838 | ||
|
|
a9afb7cba3 | ||
|
|
44bd5e04dd | ||
|
|
9be1b2cb23 | ||
|
|
92800afd95 | ||
|
|
929cb12e7e | ||
|
|
de55ba218f | ||
|
|
71fb748d70 | ||
|
|
6e341aebab | ||
|
|
a1bf28b7f0 | ||
|
|
aa90e53312 | ||
|
|
ea5b5b1f64 | ||
|
|
2205aba3ed | ||
|
|
027f51763e | ||
|
|
1a298aad9c | ||
|
|
a342867d3f | ||
|
|
b5749c75d9 | ||
|
|
3ea6f01b4e | ||
|
|
37e53513b6 | ||
|
|
1829b55bb0 | ||
|
|
6d19fe1481 | ||
|
|
781ff713ba | ||
|
|
0b9e1e7b56 | ||
|
|
c80f739461 | ||
|
|
684001ac62 | ||
|
|
f47f42090d | ||
|
|
c03c255304 | ||
|
|
fc65b68f30 | ||
|
|
130458385e | ||
|
|
480438eee6 | ||
|
|
9dd4570b68 | ||
|
|
0280176ccd | ||
|
|
b4e1c1f51e | ||
|
|
1c7bb34ffd | ||
|
|
e0fa4cf874 | ||
|
|
b3be06667d | ||
|
|
9243f0c5e3 | ||
|
|
982604fbf2 | ||
|
|
250ee2ea7d | ||
|
|
95037d8d9d | ||
|
|
8a7f7f5004 | ||
|
|
12a23f01b4 | ||
|
|
3a88808983 | ||
|
|
3be6156774 | ||
|
|
cf4c17deaf | ||
|
|
3501478828 | ||
|
|
dcf0a6fbfd | ||
|
|
4b7a5b7bfa | ||
|
|
ec1cc29ecb | ||
|
|
f286a4fcd4 | ||
|
|
e2ae8af072 | ||
|
|
585e98fe2b | ||
|
|
c407ed070c | ||
|
|
6baaa18224 | ||
|
|
584591c3e3 | ||
|
|
43369cbe06 | ||
|
|
3bfffab201 | ||
|
|
0d1d9f3e9c | ||
|
|
3bc7bba262 | ||
|
|
9c82276760 | ||
|
|
3578046101 | ||
|
|
26efd6f151 | ||
|
|
1bf6c3faad | ||
|
|
9faf780740 | ||
|
|
3ab8cfbc14 | ||
|
|
3983bae160 | ||
|
|
7346ea85c0 | ||
|
|
eb7d7ce354 | ||
|
|
b1b57a3f28 | ||
|
|
82cf76a8f9 | ||
|
|
d76e548ec1 | ||
|
|
9f633bc125 | ||
|
|
3b38d2f507 | ||
|
|
a751a80a05 | ||
|
|
77e628e840 | ||
|
|
822d0e5520 | ||
|
|
0d5c7718c0 | ||
|
|
0538a4098d | ||
|
|
300816ffa1 | ||
|
|
804199d9b6 | ||
|
|
4c3512a45c | ||
|
|
bcaea74352 | ||
|
|
c9d1ee24ca | ||
|
|
9b18151104 | ||
|
|
34a7f0ca93 | ||
|
|
5b645f9d34 | ||
|
|
284d6b279b | ||
|
|
dce6395395 | ||
|
|
6322aa154b | ||
|
|
7f01d1d8c8 | ||
|
|
069a9745b0 | ||
|
|
78087617d1 | ||
|
|
d72ce4da64 | ||
|
|
a25d1530ef | ||
|
|
d6ecbbdf0a | ||
|
|
66a5bc4fad | ||
|
|
d703e712f7 | ||
|
|
f196d77f66 | ||
|
|
0d75b9fa96 | ||
|
|
5391ccdfe6 | ||
|
|
f68dbbd3da | ||
|
|
1a32b1f002 | ||
|
|
79bf9d25db | ||
|
|
1b491e50c9 | ||
|
|
7c4ce957c7 | ||
|
|
4081413876 | ||
|
|
5dd1a738f8 | ||
|
|
8a7c1d6a00 | ||
|
|
f93aba1d66 | ||
|
|
e3b261b0b7 | ||
|
|
ee2bcdec65 | ||
|
|
beaf50f5c6 | ||
|
|
581c54bebe | ||
|
|
30bcbc433a | ||
|
|
5f7cdbe0b8 | ||
|
|
ede161d296 | ||
|
|
b5f9d47c89 | ||
|
|
e4c40158c5 | ||
|
|
cda31fb755 | ||
|
|
dada11dc5f | ||
|
|
277fd2250a | ||
|
|
073a42cc95 | ||
|
|
7fc84c7019 | ||
|
|
c06d07a276 | ||
|
|
4c7da89219 | ||
|
|
932f35a7f0 | ||
|
|
756e171ad0 | ||
|
|
4777c1cd5b | ||
|
|
b1195c125f | ||
|
|
da31b96b55 | ||
|
|
86d6232236 | ||
|
|
061e814195 | ||
|
|
56bc57cf50 | ||
|
|
27cdbf7b94 | ||
|
|
4b85c5f52c | ||
|
|
cd0afb85c4 | ||
|
|
dfea1730dc | ||
|
|
b50ea730b1 | ||
|
|
bc21350298 | ||
|
|
10afd895c4 | ||
|
|
c54d8df504 | ||
|
|
acfabfff9c | ||
|
|
65693e9e15 | ||
|
|
bf10cf5f1a | ||
|
|
2385d396c3 | ||
|
|
3a3fadcece | ||
|
|
ce5c88006e | ||
|
|
d29d41322a | ||
|
|
46ac4a2f85 | ||
|
|
da3e04df8b | ||
|
|
967b45bc1a | ||
|
|
ddf3ca7ab3 | ||
|
|
4ba5b4b55d | ||
|
|
8ad056b207 | ||
|
|
e4eb5cb443 | ||
|
|
56427b8057 | ||
|
|
65c7f78e9f | ||
|
|
8166ebd91a | ||
|
|
ddc16d8642 | ||
|
|
c77add6d21 | ||
|
|
c6eafdfbaf | ||
|
|
112c7ea315 | ||
|
|
30ad0c5674 | ||
|
|
cdd8602e74 | ||
|
|
8c793e0704 | ||
|
|
683596f91e | ||
|
|
84430a4a8a | ||
|
|
9fae76107f | ||
|
|
bd7d47fcea | ||
|
|
2b9afa775e | ||
|
|
70aa4b9231 | ||
|
|
0aacab43ca | ||
|
|
dcbdfcc9d2 | ||
|
|
7819a1010c | ||
|
|
3bffd14b02 | ||
|
|
ab6e1abe9c | ||
|
|
707cd32b13 | ||
|
|
2f5182b2d2 | ||
|
|
780548b577 | ||
|
|
0a1260b03a | ||
|
|
3167d47882 | ||
|
|
c7a7cdf734 | ||
|
|
9f94b11d4c | ||
|
|
b175179e47 | ||
|
|
d8a921f6a6 | ||
|
|
6e2ce83d57 | ||
|
|
47c7dd590d | ||
|
|
9a7f7cb74f | ||
|
|
18f0247491 | ||
|
|
731c33dd97 | ||
|
|
1952a1c68d | ||
|
|
c2c3ee8e4a | ||
|
|
211a8b288a | ||
|
|
e166e29e87 | ||
|
|
235f686da9 | ||
|
|
9613d65756 | ||
|
|
044daf4fe2 | ||
|
|
d3c7567369 | ||
|
|
bcf30b29ad | ||
|
|
b4984d5e15 | ||
|
|
464e1fcfa5 | ||
|
|
dd2cd9312a | ||
|
|
e565a4bfc4 | ||
|
|
d5da6b0cef | ||
|
|
aa337f588c | ||
|
|
4b8244fbf8 | ||
|
|
4ac80b8570 | ||
|
|
5539251d82 | ||
|
|
5c778f2f15 | ||
|
|
fdcb876495 | ||
|
|
d9d6fbb085 | ||
|
|
436b3c7d0c | ||
|
|
7b56a7a3cb | ||
|
|
10e7821461 | ||
|
|
cf890e9d43 | ||
|
|
a808c06a10 | ||
|
|
db02021aba | ||
|
|
ed25abe05f | ||
|
|
08d2f902dd | ||
|
|
0393e87519 | ||
|
|
45570e4695 | ||
|
|
64b341cc10 | ||
|
|
828101dd51 | ||
|
|
7e22afbc7c | ||
|
|
30572e28c2 | ||
|
|
d45f89c95b | ||
|
|
ab0637c2c3 | ||
|
|
1bc05aef20 | ||
|
|
772c117e68 | ||
|
|
040d985908 | ||
|
|
15a7312273 | ||
|
|
3122ff2433 | ||
|
|
027857b261 | ||
|
|
94bb4031f3 | ||
|
|
07d609cbc2 | ||
|
|
68a04b9282 | ||
|
|
399e004884 | ||
|
|
79650f795f | ||
|
|
270d302834 | ||
|
|
32fdf8efd6 | ||
|
|
61e28cdb6f | ||
|
|
118b250473 | ||
|
|
6fd730c96b | ||
|
|
48142a01dd | ||
|
|
8b69468e5f | ||
|
|
bcfaaf7da6 | ||
|
|
a85612baf8 | ||
|
|
6d28560626 | ||
|
|
86fa1138be | ||
|
|
f452899fe2 | ||
|
|
3f5ebccbff | ||
|
|
ff79437d9b | ||
|
|
5452a8ee29 | ||
|
|
00b042a3eb | ||
|
|
a53946a8a1 | ||
|
|
b8ab9f1c0a | ||
|
|
3faa2ae78c | ||
|
|
0271e8e692 | ||
|
|
74cffcf51a | ||
|
|
6d07a28a29 | ||
|
|
6200630904 | ||
|
|
7d99cee3ef | ||
|
|
99ce820cc8 | ||
|
|
ab8de33c76 | ||
|
|
32bfd567ac | ||
|
|
57f047a05a | ||
|
|
5a11a8ef69 | ||
|
|
9b61076d42 | ||
|
|
46dcb0d890 | ||
|
|
ef6a8e4f32 | ||
|
|
b9172b982f | ||
|
|
1c6ab2d759 | ||
|
|
59d3955db1 | ||
|
|
db7109c43b | ||
|
|
fd696f1243 | ||
|
|
401c16559d | ||
|
|
fa6b3490e2 | ||
|
|
4e14e38bd5 | ||
|
|
f5755bcadf | ||
|
|
9ea1de432d | ||
|
|
468d94c920 | ||
|
|
26a95988da | ||
|
|
c9ee9b45c7 | ||
|
|
02f4e3b3ff | ||
|
|
f500dd627a | ||
|
|
865469f233 | ||
|
|
67ffc00d48 | ||
|
|
389ee3624c | ||
|
|
51b0b5c5ab | ||
|
|
10efca1a74 | ||
|
|
a9512d0994 | ||
|
|
fad58dbd08 | ||
|
|
0b01c8560d | ||
|
|
3bb93abb34 | ||
|
|
f81002df60 | ||
|
|
df752a15ce | ||
|
|
d27e1ab148 | ||
|
|
3eb45eba0e | ||
|
|
d9ebe531ed | ||
|
|
7ca6d4e8f7 | ||
|
|
2b7918bd6f | ||
|
|
8fe912d95c | ||
|
|
820ef6e9d8 | ||
|
|
0a65a2384c | ||
|
|
1bc036a12d | ||
|
|
b040bd6157 | ||
|
|
3ef312fb95 | ||
|
|
91753655b7 | ||
|
|
17a4bc10bc | ||
|
|
885e0c8b76 | ||
|
|
2a0e79bbfa | ||
|
|
f64ce52305 | ||
|
|
c715660cb8 | ||
|
|
93407cf7cf | ||
|
|
a8e8d1d06c | ||
|
|
eec67a675f | ||
|
|
56424eca5c | ||
|
|
6797c7f1b1 | ||
|
|
4bfdec1eb2 | ||
|
|
7b79c0f08f | ||
|
|
8c36179d35 | ||
|
|
490f142d73 | ||
|
|
26766c22eb | ||
|
|
e006f101c3 | ||
|
|
74cc722b96 | ||
|
|
6dd50da54e | ||
|
|
f85a3757cf | ||
|
|
95cbd026cc | ||
|
|
e1f249ce20 | ||
|
|
67f42b2f26 | ||
|
|
b86d2a2d4f | ||
|
|
d4145abd33 | ||
|
|
20d0db6cfb | ||
|
|
ca025c2b1d | ||
|
|
c3a774e414 | ||
|
|
0ef54caa28 | ||
|
|
1118f02689 | ||
|
|
1cdc29e260 | ||
|
|
339dd3dc6c | ||
|
|
27047d8f51 | ||
|
|
7c4b47652e | ||
|
|
8e2d4c6da5 | ||
|
|
e76cd252fe | ||
|
|
fbdacce3fe | ||
|
|
898dde8812 | ||
|
|
d7ae9b90a0 | ||
|
|
d2bc5d6f29 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
*.pyc
|
||||
.*.swp
|
||||
|
||||
.DS_Store
|
||||
_trial_temp/
|
||||
logs/
|
||||
dbs/
|
||||
@@ -11,6 +12,14 @@ docs/build/
|
||||
|
||||
cmdclient_config.json
|
||||
homeserver*.db
|
||||
homeserver*.log
|
||||
homeserver*.pid
|
||||
homeserver*.yaml
|
||||
|
||||
*.signing.key
|
||||
*.tls.crt
|
||||
*.tls.dh
|
||||
*.tls.key
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
@@ -18,9 +27,14 @@ htmlcov
|
||||
demo/*.db
|
||||
demo/*.log
|
||||
demo/*.pid
|
||||
demo/etc
|
||||
|
||||
graph/*.svg
|
||||
graph/*.png
|
||||
graph/*.dot
|
||||
|
||||
**/webclient/config.js
|
||||
**/webclient/test/coverage/
|
||||
**/webclient/test/environment-protractor.js
|
||||
|
||||
uploads
|
||||
|
||||
274
CHANGES.rst
274
CHANGES.rst
@@ -1,3 +1,277 @@
|
||||
Changes in synapse 0.5.3c (2014-12-02)
|
||||
======================================
|
||||
|
||||
* Change the default value for the `content_addr` option to use the HTTP
|
||||
listener, as by default the HTTPS listener will be using a self-signed
|
||||
certificate.
|
||||
|
||||
Changes in synapse 0.5.3 (2014-11-27)
|
||||
=====================================
|
||||
|
||||
* Fix bug that caused joining a remote room to fail if a single event was not
|
||||
signed correctly.
|
||||
* Fix bug which caused servers to continuously try and fetch events from other
|
||||
servers.
|
||||
|
||||
Changes in synapse 0.5.2 (2014-11-26)
|
||||
=====================================
|
||||
|
||||
Fix major bug that caused rooms to disappear from peoples initial sync.
|
||||
|
||||
Changes in synapse 0.5.1 (2014-11-26)
|
||||
=====================================
|
||||
See UPGRADES.rst for specific instructions on how to upgrade.
|
||||
|
||||
* Fix bug where we served up an Event that did not match its signatures.
|
||||
* Fix regression where we no longer correctly handled the case where a
|
||||
homeserver receives an event for a room it doesn't recognise (but is in.)
|
||||
|
||||
Changes in synapse 0.5.0 (2014-11-19)
|
||||
=====================================
|
||||
This release includes changes to the federation protocol and client-server API
|
||||
that is not backwards compatible.
|
||||
|
||||
This release also changes the internal database schemas and so requires servers to
|
||||
drop their current history. See UPGRADES.rst for details.
|
||||
|
||||
Homeserver:
|
||||
* Add authentication and authorization to the federation protocol. Events are
|
||||
now signed by their originating homeservers.
|
||||
* Implement the new authorization model for rooms.
|
||||
* Split out web client into a seperate repository: matrix-angular-sdk.
|
||||
* Change the structure of PDUs.
|
||||
* Fix bug where user could not join rooms via an alias containing 4-byte
|
||||
UTF-8 characters.
|
||||
* Merge concept of PDUs and Events internally.
|
||||
* Improve logging by adding request ids to log lines.
|
||||
* Implement a very basic room initial sync API.
|
||||
* Implement the new invite/join federation APIs.
|
||||
|
||||
Webclient:
|
||||
* The webclient has been moved to a seperate repository.
|
||||
|
||||
Changes in synapse 0.4.2 (2014-10-31)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* Fix bugs where we did not notify users of correct presence updates.
|
||||
* Fix bug where we did not handle sub second event stream timeouts.
|
||||
|
||||
Webclient:
|
||||
* Add ability to click on messages to see JSON.
|
||||
* Add ability to redact messages.
|
||||
* Add ability to view and edit all room state JSON.
|
||||
* Handle incoming redactions.
|
||||
* Improve feedback on errors.
|
||||
* Fix bugs in mobile CSS.
|
||||
* Fix bugs with desktop notifications.
|
||||
|
||||
Changes in synapse 0.4.1 (2014-10-17)
|
||||
=====================================
|
||||
Webclient:
|
||||
* Fix bug with display of timestamps.
|
||||
|
||||
Changes in synpase 0.4.0 (2014-10-17)
|
||||
=====================================
|
||||
This release includes changes to the federation protocol and client-server API
|
||||
that is not backwards compatible.
|
||||
|
||||
The Matrix specification has been moved to a separate git repository:
|
||||
http://github.com/matrix-org/matrix-doc
|
||||
|
||||
You will also need an updated syutil and config. See UPGRADES.rst.
|
||||
|
||||
Homeserver:
|
||||
* Sign federation transactions to assert strong identity over federation.
|
||||
* Rename timestamp keys in PDUs and events from 'ts' and 'hsob_ts' to 'origin_server_ts'.
|
||||
|
||||
|
||||
Changes in synapse 0.3.4 (2014-09-25)
|
||||
=====================================
|
||||
This version adds support for using a TURN server. See docs/turn-howto.rst on
|
||||
how to set one up.
|
||||
|
||||
Homeserver:
|
||||
* Add support for redaction of messages.
|
||||
* Fix bug where inviting a user on a remote home server could take up to
|
||||
20-30s.
|
||||
* Implement a get current room state API.
|
||||
* Add support specifying and retrieving turn server configuration.
|
||||
|
||||
Webclient:
|
||||
* Add button to send messages to users from the home page.
|
||||
* Add support for using TURN for VoIP calls.
|
||||
* Show display name change messages.
|
||||
* Fix bug where the client didn't get the state of a newly joined room
|
||||
until after it has been refreshed.
|
||||
* Fix bugs with tab complete.
|
||||
* Fix bug where holding down the down arrow caused chrome to chew 100% CPU.
|
||||
* Fix bug where desktop notifications occasionally used "Undefined" as the
|
||||
display name.
|
||||
* Fix more places where we sometimes saw room IDs incorrectly.
|
||||
* Fix bug which caused lag when entering text in the text box.
|
||||
|
||||
Changes in synapse 0.3.3 (2014-09-22)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* Fix bug where you continued to get events for rooms you had left.
|
||||
|
||||
Webclient:
|
||||
* Add support for video calls with basic UI.
|
||||
* Fix bug where one to one chats were named after your display name rather
|
||||
than the other person's.
|
||||
* Fix bug which caused lag when typing in the textarea.
|
||||
* Refuse to run on browsers we know won't work.
|
||||
* Trigger pagination when joining new rooms.
|
||||
* Fix bug where we sometimes didn't display invitations in recents.
|
||||
* Automatically join room when accepting a VoIP call.
|
||||
* Disable outgoing and reject incoming calls on browsers we don't support
|
||||
VoIP in.
|
||||
* Don't display desktop notifications for messages in the room you are
|
||||
non-idle and speaking in.
|
||||
|
||||
Changes in synapse 0.3.2 (2014-09-18)
|
||||
=====================================
|
||||
|
||||
Webclient:
|
||||
* Fix bug where an empty "bing words" list in old accounts didn't send
|
||||
notifications when it should have done.
|
||||
|
||||
Changes in synapse 0.3.1 (2014-09-18)
|
||||
=====================================
|
||||
This is a release to hotfix v0.3.0 to fix two regressions.
|
||||
|
||||
Webclient:
|
||||
* Fix a regression where we sometimes displayed duplicate events.
|
||||
* Fix a regression where we didn't immediately remove rooms you were
|
||||
banned in from the recents list.
|
||||
|
||||
Changes in synapse 0.3.0 (2014-09-18)
|
||||
=====================================
|
||||
See UPGRADE for information about changes to the client server API, including
|
||||
breaking backwards compatibility with VoIP calls and registration API.
|
||||
|
||||
Homeserver:
|
||||
* When a user changes their displayname or avatar the server will now update
|
||||
all their join states to reflect this.
|
||||
* The server now adds "age" key to events to indicate how old they are. This
|
||||
is clock independent, so at no point does any server or webclient have to
|
||||
assume their clock is in sync with everyone else.
|
||||
* Fix bug where we didn't correctly pull in missing PDUs.
|
||||
* Fix bug where prev_content key wasn't always returned.
|
||||
* Add support for password resets.
|
||||
|
||||
Webclient:
|
||||
* Improve page content loading.
|
||||
* Join/parts now trigger desktop notifications.
|
||||
* Always show room aliases in the UI if one is present.
|
||||
* No longer show user-count in the recents side panel.
|
||||
* Add up & down arrow support to the text box for message sending to step
|
||||
through your sent history.
|
||||
* Don't display notifications for our own messages.
|
||||
* Emotes are now formatted correctly in desktop notifications.
|
||||
* The recents list now differentiates between public & private rooms.
|
||||
* Fix bug where when switching between rooms the pagination flickered before
|
||||
the view jumped to the bottom of the screen.
|
||||
* Add bing word support.
|
||||
|
||||
Registration API:
|
||||
* The registration API has been overhauled to function like the login API. In
|
||||
practice, this means registration requests must now include the following:
|
||||
'type':'m.login.password'. See UPGRADE for more information on this.
|
||||
* The 'user_id' key has been renamed to 'user' to better match the login API.
|
||||
* There is an additional login type: 'm.login.email.identity'.
|
||||
* The command client and web client have been updated to reflect these changes.
|
||||
|
||||
Changes in synapse 0.2.3 (2014-09-12)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* Fix bug where we stopped sending events to remote home servers if a
|
||||
user from that home server left, even if there were some still in the
|
||||
room.
|
||||
* Fix bugs in the state conflict resolution where it was incorrectly
|
||||
rejecting events.
|
||||
|
||||
Webclient:
|
||||
* Display room names and topics.
|
||||
* Allow setting/editing of room names and topics.
|
||||
* Display information about rooms on the main page.
|
||||
* Handle ban and kick events in real time.
|
||||
* VoIP UI and reliability improvements.
|
||||
* Add glare support for VoIP.
|
||||
* Improvements to initial startup speed.
|
||||
* Don't display duplicate join events.
|
||||
* Local echo of messages.
|
||||
* Differentiate sending and sent of local echo.
|
||||
* Various minor bug fixes.
|
||||
|
||||
Changes in synapse 0.2.2 (2014-09-06)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* When the server returns state events it now also includes the previous
|
||||
content.
|
||||
* Add support for inviting people when creating a new room.
|
||||
* Make the homeserver inform the room via `m.room.aliases` when a new alias
|
||||
is added for a room.
|
||||
* Validate `m.room.power_level` events.
|
||||
|
||||
Webclient:
|
||||
* Add support for captchas on registration.
|
||||
* Handle `m.room.aliases` events.
|
||||
* Asynchronously send messages and show a local echo.
|
||||
* Inform the UI when a message failed to send.
|
||||
* Only autoscroll on receiving a new message if the user was already at the
|
||||
bottom of the screen.
|
||||
* Add support for ban/kick reasons.
|
||||
|
||||
Changes in synapse 0.2.1 (2014-09-03)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* Added support for signing up with a third party id.
|
||||
* Add synctl scripts.
|
||||
* Added rate limiting.
|
||||
* Add option to change the external address the content repo uses.
|
||||
* Presence bug fixes.
|
||||
|
||||
Webclient:
|
||||
* Added support for signing up with a third party id.
|
||||
* Added support for banning and kicking users.
|
||||
* Added support for displaying and setting ops.
|
||||
* Added support for room names.
|
||||
* Fix bugs with room membership event display.
|
||||
|
||||
Changes in synapse 0.2.0 (2014-09-02)
|
||||
=====================================
|
||||
This update changes many configuration options, updates the
|
||||
database schema and mandates SSL for server-server connections.
|
||||
|
||||
Homeserver:
|
||||
* Require SSL for server-server connections.
|
||||
* Add SSL listener for client-server connections.
|
||||
* Add ability to use config files.
|
||||
* Add support for kicking/banning and power levels.
|
||||
* Allow setting of room names and topics on creation.
|
||||
* Change presence to include last seen time of the user.
|
||||
* Change url path prefix to /_matrix/...
|
||||
* Bug fixes to presence.
|
||||
|
||||
Webclient:
|
||||
* Reskin the CSS for registration and login.
|
||||
* Various improvements to rooms CSS.
|
||||
* Support changes in client-server API.
|
||||
* Bug fixes to VOIP UI.
|
||||
* Various bug fixes to handling of changes to room member list.
|
||||
|
||||
Changes in synapse 0.1.2 (2014-08-29)
|
||||
=====================================
|
||||
|
||||
Webclient:
|
||||
* Add basic call state UI for VoIP calls.
|
||||
|
||||
Changes in synapse 0.1.1 (2014-08-29)
|
||||
=====================================
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
recursive-include docs *
|
||||
recursive-include tests *.py
|
||||
recursive-include synapse/persistence/schema *.sql
|
||||
recursive-include synapse/storage/schema *.sql
|
||||
recursive-include syweb/webclient *
|
||||
|
||||
271
README.rst
271
README.rst
@@ -1,65 +1,50 @@
|
||||
Quick Start
|
||||
===========
|
||||
Introduction
|
||||
============
|
||||
|
||||
Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
||||
VoIP[1]. The basics you need to know to get up and running are:
|
||||
VoIP. The basics you need to know to get up and running are:
|
||||
|
||||
- Chatrooms are distributed and do not exist on any single server. Rooms
|
||||
can be found using names like ``#matrix:matrix.org`` or
|
||||
``#test:localhost:8080`` or they can be ephemeral.
|
||||
|
||||
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
||||
you will normally refer to yourself and others using a 3PID: email
|
||||
address, phone number, etc rather than manipulating Matrix user IDs)
|
||||
- Everything in Matrix happens in a room. Rooms are distributed and do not
|
||||
exist on any single server. Rooms can be located using convenience aliases
|
||||
like ``#matrix:matrix.org`` or ``#test:localhost:8008``.
|
||||
|
||||
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
||||
you will normally refer to yourself and others using a 3PID: email
|
||||
address, phone number, etc rather than manipulating Matrix user IDs)
|
||||
|
||||
The overall architecture is::
|
||||
|
||||
client <----> homeserver <=================> homeserver <-----> client
|
||||
e.g. matrix.org:8080 e.g. mydomain.net:8080
|
||||
client <----> homeserver <=====================> homeserver <----> client
|
||||
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
||||
|
||||
To get up and running:
|
||||
|
||||
- To simply play with an **existing** homeserver you can
|
||||
just go straight to http://matrix.org/alpha.
|
||||
|
||||
- To run your own **private** homeserver on localhost:8080, install synapse
|
||||
with ``python setup.py develop --user`` and then run one with
|
||||
``python synapse/app/homeserver.py`` - you will find a webclient running
|
||||
at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
|
||||
please...)
|
||||
|
||||
- To make the homeserver **public** and let it exchange messages with
|
||||
other homeservers and participate in the overall Matrix federation, open
|
||||
up port 8080 and run ``python synapse/app/homeserver.py --host
|
||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
||||
say hi! :)
|
||||
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
||||
accessed by the web client at http://matrix.org/alpha or via an IRC bridge at
|
||||
irc://irc.freenode.net/matrix.
|
||||
|
||||
For more detailed setup instructions, please see further down this document.
|
||||
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!
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
|
||||
About Matrix
|
||||
============
|
||||
|
||||
Matrix specifies a set of pragmatic RESTful HTTP JSON APIs as an open standard,
|
||||
which handle:
|
||||
|
||||
- Creating and managing fully distributed chat rooms with no
|
||||
single points of control or failure
|
||||
- Eventually-consistent cryptographically secure[2] synchronisation of room
|
||||
state across a global open network of federated servers and services
|
||||
- Sending and receiving extensible messages in a room with (optional)
|
||||
end-to-end encryption[3]
|
||||
- Inviting, joining, leaving, kicking, banning room members
|
||||
- Managing user accounts (registration, login, logout)
|
||||
- Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
|
||||
Facebook accounts to authenticate, identify and discover users on Matrix.
|
||||
- Placing 1:1 VoIP and Video calls (in development)
|
||||
- Creating and managing fully distributed chat rooms with no
|
||||
single points of control or failure
|
||||
- Eventually-consistent cryptographically secure synchronisation of room
|
||||
state across a global open network of federated servers and services
|
||||
- Sending and receiving extensible messages in a room with (optional)
|
||||
end-to-end encryption[1]
|
||||
- Inviting, joining, leaving, kicking, banning room members
|
||||
- Managing user accounts (registration, login, logout)
|
||||
- Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
|
||||
Facebook accounts to authenticate, identify and discover users on Matrix.
|
||||
- Placing 1:1 VoIP and Video calls
|
||||
|
||||
These APIs are intended to be implemented on a wide range of servers, services
|
||||
and clients, letting developers build messaging and VoIP functionality on top of
|
||||
the entirely open Matrix ecosystem rather than using closed or proprietary
|
||||
and clients, letting developers build messaging and VoIP functionality on top
|
||||
of the entirely open Matrix ecosystem rather than using closed or proprietary
|
||||
solutions. The hope is for Matrix to act as the building blocks for a new
|
||||
generation of fully open and interoperable messaging and VoIP apps for the
|
||||
internet.
|
||||
@@ -74,61 +59,129 @@ In Matrix, every user runs one or more Matrix clients, which connect through to
|
||||
a Matrix homeserver which stores all their personal chat history and user
|
||||
account information - much as a mail client connects through to an IMAP/SMTP
|
||||
server. Just like email, you can either run your own Matrix homeserver and
|
||||
control and own your own communications and history or use one hosted by someone
|
||||
else (e.g. matrix.org) - there is no single point of control or mandatory
|
||||
service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc.
|
||||
control and own your own communications and history or use one hosted by
|
||||
someone else (e.g. matrix.org) - there is no single point of control or
|
||||
mandatory service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc.
|
||||
|
||||
Synapse ships with two basic demo Matrix clients: webclient (a basic group chat
|
||||
web client demo implemented in AngularJS) and cmdclient (a basic Python
|
||||
commandline utility which lets you easily see what the JSON APIs are up to).
|
||||
command line utility which lets you easily see what the JSON APIs are up to).
|
||||
|
||||
We'd like to invite you to take a look at the Matrix spec, try to run a
|
||||
homeserver, and join the existing Matrix chatrooms already out there, experiment
|
||||
with the APIs and the demo clients, and let us know your thoughts at
|
||||
https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
|
||||
Meanwhile, iOS and Android SDKs and clients are currently in development and available from:
|
||||
|
||||
Thanks for trying Matrix!
|
||||
- https://github.com/matrix-org/matrix-ios-sdk
|
||||
- https://github.com/matrix-org/matrix-android-sdk
|
||||
|
||||
[2] Cryptographic signing of messages isn't turned on yet
|
||||
We'd like to invite you to join #matrix:matrix.org (via http://matrix.org/alpha), run a homeserver, take a look at the Matrix spec at
|
||||
http://matrix.org/docs/spec, experiment with the APIs and the demo
|
||||
clients, and report any bugs via http://matrix.org/jira.
|
||||
|
||||
[3] End-to-end encryption is currently in development
|
||||
Thanks for using Matrix!
|
||||
|
||||
[1] End-to-end encryption is currently in development
|
||||
|
||||
Homeserver Installation
|
||||
=======================
|
||||
|
||||
First, the dependencies need to be installed. Start by installing
|
||||
'python2.7-dev' and the various tools of the compiler toolchain.
|
||||
N.B. synapse requires python 2.x where x >= 7
|
||||
System requirements:
|
||||
- POSIX-compliant system (tested on Linux & OSX)
|
||||
- Python 2.7
|
||||
|
||||
Installing prerequisites on ubuntu::
|
||||
Synapse is written in python but some of the libraries is uses are written in
|
||||
C. So before we can install synapse itself we need a working C compiler and the
|
||||
header files for python C extensions.
|
||||
|
||||
$ sudo apt-get install build-essential python2.7-dev libffi-dev
|
||||
Installing prerequisites on Ubuntu or Debian::
|
||||
|
||||
Installing prerequisites on Mac OS X::
|
||||
$ sudo apt-get install build-essential python2.7-dev libffi-dev \
|
||||
python-pip python-setuptools sqlite3 \
|
||||
libssl-dev
|
||||
|
||||
Installing prerequisites on Mac OS X::
|
||||
|
||||
$ xcode-select --install
|
||||
|
||||
To install the synapse homeserver run::
|
||||
|
||||
$ pip install --user --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
||||
|
||||
This installs synapse, along with the libraries it uses, into
|
||||
``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX.
|
||||
|
||||
Troubleshooting Installation
|
||||
----------------------------
|
||||
|
||||
Synapse requires pip 1.7 or later, so if your OS provides too old a version and
|
||||
you get errors about ``error: no such option: --process-dependency-links`` you
|
||||
may need to manually upgrade it::
|
||||
|
||||
$ sudo pip install --upgrade pip
|
||||
|
||||
If pip crashes mid-installation for reason (e.g. lost terminal), pip may
|
||||
refuse to run until you remove the temporary installation directory it
|
||||
created. To reset the installation::
|
||||
|
||||
$ rm -rf /tmp/pip_install_matrix
|
||||
|
||||
pip seems to leak *lots* of memory during installation. For instance, a Linux
|
||||
host with 512MB of RAM may run out of memory whilst installing Twisted. If this
|
||||
happens, you will have to individually install the dependencies which are
|
||||
failing, e.g.::
|
||||
|
||||
$ pip install --user twisted
|
||||
|
||||
Running Your Homeserver
|
||||
=======================
|
||||
|
||||
To actually run your new homeserver, pick a working directory for Synapse to run
|
||||
(e.g. ``~/.synapse``), and::
|
||||
|
||||
$ mkdir ~/.synapse
|
||||
$ cd ~/.synapse
|
||||
|
||||
$ # on Linux
|
||||
$ ~/.local/bin/synctl start
|
||||
|
||||
$ # on OSX
|
||||
$ ~/Library/Python/2.7/bin/synctl start
|
||||
|
||||
Troubleshooting Running
|
||||
-----------------------
|
||||
|
||||
If ``synctl`` fails with ``pkg_resources.DistributionNotFound`` errors you may
|
||||
need a newer version of setuptools than that provided by your OS.::
|
||||
|
||||
$ sudo pip install setuptools --upgrade
|
||||
|
||||
If synapse fails with ``missing "sodium.h"`` crypto errors, you may need
|
||||
to manually upgrade PyNaCL, as synapse uses NaCl (http://nacl.cr.yp.to/) for
|
||||
encryption and digital signatures.
|
||||
Unfortunately PyNACL currently has a few issues
|
||||
(https://github.com/pyca/pynacl/issues/53) and
|
||||
(https://github.com/pyca/pynacl/issues/79) that mean it may not install
|
||||
correctly, causing all tests to fail with errors about missing "sodium.h". To
|
||||
fix try re-installing from PyPI or directly from
|
||||
(https://github.com/pyca/pynacl)::
|
||||
|
||||
$ # Install from PyPI
|
||||
$ pip install --user --upgrade --force pynacl
|
||||
$ # Install from github
|
||||
$ pip install --user https://github.com/pyca/pynacl/tarball/master
|
||||
|
||||
|
||||
Homeserver Development
|
||||
======================
|
||||
|
||||
To check out a homeserver for development, clone the git repo into a working
|
||||
directory of your choice::
|
||||
|
||||
$ git clone https://github.com/matrix-org/synapse.git
|
||||
$ cd synapse
|
||||
|
||||
The homeserver has a number of external dependencies, that are easiest
|
||||
to install by making setup.py do so, in --user mode::
|
||||
|
||||
$ python setup.py develop --user
|
||||
|
||||
You'll need a version of setuptools new enough to know about git, so you
|
||||
may need to also run:
|
||||
|
||||
$ sudo apt-get install python-pip
|
||||
$ sudo pip install --upgrade setuptools
|
||||
|
||||
If you don't have access to github, then you may need to install ``syutil``
|
||||
manually by checking it out and running ``python setup.py develop --user`` on it
|
||||
too.
|
||||
|
||||
If you get errors about ``sodium.h`` being missing, you may also need to
|
||||
manually install a newer PyNaCl via pip as setuptools installs an old one. Or
|
||||
you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
|
||||
installing it. Installing PyNaCl using pip may also work (remember to remove any
|
||||
other versions installed by setuputils in, for example, ~/.local/lib).
|
||||
|
||||
This will run a process of downloading and installing into your
|
||||
user's .local/lib directory all of the required dependencies that are
|
||||
@@ -149,9 +202,9 @@ This should end with a 'PASSED' result::
|
||||
Upgrading an existing homeserver
|
||||
================================
|
||||
|
||||
Before upgrading an existing homeserver to a new version, please refer to
|
||||
UPGRADE.rst for any additional instructions.
|
||||
|
||||
IMPORTANT: Before upgrading an existing homeserver to a new version, please
|
||||
refer to UPGRADE.rst for any additional instructions.
|
||||
|
||||
|
||||
Setting up Federation
|
||||
=====================
|
||||
@@ -161,19 +214,25 @@ be publicly visible on the internet, and they will need to know its host name.
|
||||
You have two choices here, which will influence the form of your Matrix user
|
||||
IDs:
|
||||
|
||||
1) Use the machine's own hostname as available on public DNS in the form of its
|
||||
A or AAAA records. This is easier to set up initially, perhaps for testing,
|
||||
but lacks the flexibility of SRV.
|
||||
1) Use the machine's own hostname as available on public DNS in the form of
|
||||
its A or AAAA records. This is easier to set up initially, perhaps for
|
||||
testing, but lacks the flexibility of SRV.
|
||||
|
||||
2) Set up a SRV record for your domain name. This requires you create a SRV
|
||||
record in DNS, but gives the flexibility to run the server on your own
|
||||
choice of TCP port, on a machine that might not be the same name as the
|
||||
domain name.
|
||||
2) Set up a SRV record for your domain name. This requires you create a SRV
|
||||
record in DNS, but gives the flexibility to run the server on your own
|
||||
choice of TCP port, on a machine that might not be the same name as the
|
||||
domain name.
|
||||
|
||||
For the first form, simply pass the required hostname (of the machine) as the
|
||||
--host parameter::
|
||||
--server-name parameter::
|
||||
|
||||
$ python synapse/app/homeserver.py --host machine.my.domain.name
|
||||
$ python -m synapse.app.homeserver \
|
||||
--server-name machine.my.domain.name \
|
||||
--config-path homeserver.config \
|
||||
--generate-config
|
||||
$ python -m synapse.app.homeserver --config-path homeserver.config
|
||||
|
||||
Alternatively, you can run ``synctl start`` to guide you through the process.
|
||||
|
||||
For the second form, first create your SRV record and publish it in DNS. This
|
||||
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
|
||||
@@ -181,12 +240,20 @@ and port where the server is running. (At the current time synapse does not
|
||||
support clustering multiple servers into a single logical homeserver). The DNS
|
||||
record would then look something like::
|
||||
|
||||
$ dig -t srv _matrix._tcp.machine.my.domaine.name
|
||||
_matrix._tcp IN SRV 10 0 8448 machine.my.domain.name.
|
||||
|
||||
|
||||
At this point, you should then run the homeserver with the hostname of this
|
||||
SRV record, as that is the name other machines will expect it to have::
|
||||
|
||||
$ python synapse/app/homeserver.py --host my.domain.name --port 8448
|
||||
$ python -m synapse.app.homeserver \
|
||||
--server-name YOURDOMAIN \
|
||||
--bind-port 8448 \
|
||||
--config-path homeserver.config \
|
||||
--generate-config
|
||||
$ python -m synapse.app.homeserver --config-path homeserver.config
|
||||
|
||||
|
||||
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.
|
||||
@@ -204,11 +271,13 @@ private federation (``localhost:8080``, ``localhost:8081`` and
|
||||
http://localhost:8080. Simply run::
|
||||
|
||||
$ demo/start.sh
|
||||
|
||||
This is mainly useful just for development purposes.
|
||||
|
||||
Running The Demo Web Client
|
||||
===========================
|
||||
|
||||
The homeserver runs a web client by default at http://localhost:8080.
|
||||
The homeserver runs a web client by default at https://localhost:8448/.
|
||||
|
||||
If this is the first time you have used the client from that browser (it uses
|
||||
HTML5 local storage to remember its config), you will need to log in to your
|
||||
@@ -228,8 +297,8 @@ account. Your name will take the form of::
|
||||
|
||||
Specify your desired localpart in the topmost box of the "Register for an
|
||||
account" form, and click the "Register" button. Hostnames can contain ports if
|
||||
required due to lack of SRV records (e.g. @matthew:localhost:8080 on an internal
|
||||
synapse sandbox running on localhost)
|
||||
required due to lack of SRV records (e.g. @matthew:localhost:8448 on an
|
||||
internal synapse sandbox running on localhost)
|
||||
|
||||
|
||||
Logging In To An Existing Account
|
||||
@@ -244,9 +313,9 @@ Identity Servers
|
||||
|
||||
The job of authenticating 3PIDs and tracking which 3PIDs are associated with a
|
||||
given Matrix user is very security-sensitive, as there is obvious risk of spam
|
||||
if it is too easy to sign up for Matrix accounts or harvest 3PID data. Meanwhile
|
||||
the job of publishing the end-to-end encryption public keys for Matrix users is
|
||||
also very security-sensitive for similar reasons.
|
||||
if it is too easy to sign up for Matrix accounts or harvest 3PID data.
|
||||
Meanwhile the job of publishing the end-to-end encryption public keys for
|
||||
Matrix users is also very security-sensitive for similar reasons.
|
||||
|
||||
Therefore the role of managing trusted identity in the Matrix ecosystem is
|
||||
farmed out to a cluster of known trusted ecosystem partners, who run 'Matrix
|
||||
@@ -255,19 +324,21 @@ track 3PID logins and publish end-user public keys.
|
||||
|
||||
It's currently early days for identity servers as Matrix is not yet using 3PIDs
|
||||
as the primary means of identity and E2E encryption is not complete. As such,
|
||||
we're not yet running an identity server in public.
|
||||
we are running a single identity server (http://matrix.org:8090) at the current
|
||||
time.
|
||||
|
||||
|
||||
Where's the spec?!
|
||||
==================
|
||||
|
||||
For now, please go spelunking in the ``docs/`` directory to find out.
|
||||
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
||||
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
||||
|
||||
|
||||
Building Internal API Documentation
|
||||
===================================
|
||||
|
||||
Before building internal API documentation install spinx and
|
||||
Before building internal API documentation install sphinx and
|
||||
sphinxcontrib-napoleon::
|
||||
|
||||
$ pip install sphinx
|
||||
|
||||
115
UPGRADE.rst
115
UPGRADE.rst
@@ -1,3 +1,118 @@
|
||||
Upgrading to v0.5.1
|
||||
===================
|
||||
|
||||
Depending on precisely when you installed v0.5.0 you may have ended up with
|
||||
a stale release of the reference matrix webclient installed as a python module.
|
||||
To uninstall it and ensure you are depending on the latest module, please run::
|
||||
|
||||
$ pip uninstall syweb
|
||||
|
||||
Upgrading to v0.5.0
|
||||
===================
|
||||
|
||||
The webclient has been split out into a seperate repository/pacakage in this
|
||||
release. Before you restart your homeserver you will need to pull in the
|
||||
webclient package by running::
|
||||
|
||||
python setup.py develop --user
|
||||
|
||||
This release completely changes the database schema and so requires upgrading
|
||||
it before starting the new version of the homeserver.
|
||||
|
||||
The script "database-prepare-for-0.5.0.sh" should be used to upgrade the
|
||||
database. This will save all user information, such as logins and profiles,
|
||||
but will otherwise purge the database. This includes messages, which
|
||||
rooms the home server was a member of and room alias mappings.
|
||||
|
||||
If you would like to keep your history, please take a copy of your database
|
||||
file and ask for help in #matrix:matrix.org. The upgrade process is,
|
||||
unfortunately, non trivial and requires human intervention to resolve any
|
||||
resulting conflicts during the upgrade process.
|
||||
|
||||
Before running the command the homeserver should be first completely
|
||||
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||
|
||||
./database-prepare-for-0.5.0.sh "homeserver.db"
|
||||
|
||||
Once this has successfully completed it will be safe to restart the
|
||||
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||
restart than usual as it reinitializes the database.
|
||||
|
||||
On startup of the new version, users can either rejoin remote rooms using room
|
||||
aliases or by being reinvited. Alternatively, if any other homeserver sends a
|
||||
message to a room that the homeserver was previously in the local HS will
|
||||
automatically rejoin the room.
|
||||
|
||||
Upgrading to v0.4.0
|
||||
===================
|
||||
|
||||
This release needs an updated syutil version. Run::
|
||||
|
||||
python setup.py develop
|
||||
|
||||
You will also need to upgrade your configuration as the signing key format has
|
||||
changed. Run::
|
||||
|
||||
python -m synapse.app.homeserver --config-path <CONFIG> --generate-config
|
||||
|
||||
|
||||
Upgrading to v0.3.0
|
||||
===================
|
||||
|
||||
This registration API now closely matches the login API. This introduces a bit
|
||||
more backwards and forwards between the HS and the client, but this improves
|
||||
the overall flexibility of the API. You can now GET on /register to retrieve a list
|
||||
of valid registration flows. Upon choosing one, they are submitted in the same
|
||||
way as login, e.g::
|
||||
|
||||
{
|
||||
type: m.login.password,
|
||||
user: foo,
|
||||
password: bar
|
||||
}
|
||||
|
||||
The default HS supports 2 flows, with and without Identity Server email
|
||||
authentication. Enabling captcha on the HS will add in an extra step to all
|
||||
flows: ``m.login.recaptcha`` which must be completed before you can transition
|
||||
to the next stage. There is a new login type: ``m.login.email.identity`` which
|
||||
contains the ``threepidCreds`` key which were previously sent in the original
|
||||
register request. For more information on this, see the specification.
|
||||
|
||||
Web Client
|
||||
----------
|
||||
|
||||
The VoIP specification has changed between v0.2.0 and v0.3.0. Users should
|
||||
refresh any browser tabs to get the latest web client code. Users on
|
||||
v0.2.0 of the web client will not be able to call those on v0.3.0 and
|
||||
vice versa.
|
||||
|
||||
|
||||
Upgrading to v0.2.0
|
||||
===================
|
||||
|
||||
The home server now requires setting up of SSL config before it can run. To
|
||||
automatically generate default config use::
|
||||
|
||||
$ python synapse/app/homeserver.py \
|
||||
--server-name machine.my.domain.name \
|
||||
--bind-port 8448 \
|
||||
--config-path homeserver.config \
|
||||
--generate-config
|
||||
|
||||
This config can be edited if desired, for example to specify a different SSL
|
||||
certificate to use. Once done you can run the home server using::
|
||||
|
||||
$ python synapse/app/homeserver.py --config-path homeserver.config
|
||||
|
||||
See the README.rst for more information.
|
||||
|
||||
Also note that some config options have been renamed, including:
|
||||
|
||||
- "host" to "server-name"
|
||||
- "database" to "database-path"
|
||||
- "port" to "bind-port" and "unsecure-port"
|
||||
|
||||
|
||||
Upgrading to v0.0.1
|
||||
===================
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -60,7 +60,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
"complete_usernames": "on",
|
||||
"send_delivery_receipts": "on"
|
||||
}
|
||||
self.path_prefix = "/matrix/client/api/v1"
|
||||
self.path_prefix = "/_matrix/client/api/v1"
|
||||
self.event_stream_token = "END"
|
||||
self.prompt = ">>> "
|
||||
|
||||
@@ -88,6 +88,8 @@ class SynapseCmd(cmd.Cmd):
|
||||
return False
|
||||
|
||||
def _domain(self):
|
||||
if "user" not in self.config or not self.config["user"]:
|
||||
return None
|
||||
return self.config["user"].split(":")[1]
|
||||
|
||||
def do_config(self, line):
|
||||
@@ -143,35 +145,50 @@ class SynapseCmd(cmd.Cmd):
|
||||
<noupdate> : Do not automatically clobber config values.
|
||||
"""
|
||||
args = self._parse(line, ["userid", "noupdate"])
|
||||
path = "/register"
|
||||
|
||||
password = None
|
||||
pwd = None
|
||||
pwd2 = "_"
|
||||
while pwd != pwd2:
|
||||
pwd = getpass.getpass("(Optional) Type a password for this user: ")
|
||||
if len(pwd) == 0:
|
||||
print "Not using a password for this user."
|
||||
break
|
||||
pwd = getpass.getpass("Type a password for this user: ")
|
||||
pwd2 = getpass.getpass("Retype the password: ")
|
||||
if pwd != pwd2:
|
||||
if pwd != pwd2 or len(pwd) == 0:
|
||||
print "Password mismatch."
|
||||
pwd = None
|
||||
else:
|
||||
password = pwd
|
||||
|
||||
body = {}
|
||||
body = {
|
||||
"type": "m.login.password"
|
||||
}
|
||||
if "userid" in args:
|
||||
body["user_id"] = args["userid"]
|
||||
body["user"] = args["userid"]
|
||||
if password:
|
||||
body["password"] = password
|
||||
|
||||
reactor.callFromThread(self._do_register, "POST", path, body,
|
||||
reactor.callFromThread(self._do_register, body,
|
||||
"noupdate" not in args)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_register(self, method, path, data, update_config):
|
||||
url = self._url() + path
|
||||
json_res = yield self.http_client.do_request(method, url, data=data)
|
||||
def _do_register(self, data, update_config):
|
||||
# check the registration flows
|
||||
url = self._url() + "/register"
|
||||
json_res = yield self.http_client.do_request("GET", url)
|
||||
print json.dumps(json_res, indent=4)
|
||||
|
||||
passwordFlow = None
|
||||
for flow in json_res["flows"]:
|
||||
if flow["type"] == "m.login.recaptcha" or ("stages" in flow and "m.login.recaptcha" in flow["stages"]):
|
||||
print "Unable to register: Home server requires captcha."
|
||||
return
|
||||
if flow["type"] == "m.login.password" and "stages" not in flow:
|
||||
passwordFlow = flow
|
||||
break
|
||||
|
||||
if not passwordFlow:
|
||||
return
|
||||
|
||||
json_res = yield self.http_client.do_request("POST", url, data=data)
|
||||
print json.dumps(json_res, indent=4)
|
||||
if update_config and "user_id" in json_res:
|
||||
self.config["user"] = json_res["user_id"]
|
||||
@@ -191,10 +208,12 @@ class SynapseCmd(cmd.Cmd):
|
||||
p = getpass.getpass("Enter your password: ")
|
||||
user = args["user_id"]
|
||||
if self._is_on("complete_usernames") and not user.startswith("@"):
|
||||
user = "@" + user + ":" + self._domain()
|
||||
|
||||
domain = self._domain()
|
||||
if domain:
|
||||
user = "@" + user + ":" + domain
|
||||
|
||||
reactor.callFromThread(self._do_login, user, p)
|
||||
print " got %s " % p
|
||||
#print " got %s " % p
|
||||
except Exception as e:
|
||||
print e
|
||||
|
||||
@@ -252,7 +271,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_emailrequest(self, args):
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
|
||||
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken"
|
||||
|
||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
@@ -274,7 +293,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_emailvalidate(self, args):
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
|
||||
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken"
|
||||
|
||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
@@ -294,7 +313,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidbind(self, args):
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind"
|
||||
url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind"
|
||||
|
||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
@@ -312,7 +331,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
try:
|
||||
args = self._parse(line, ["roomname"], force_keys=True)
|
||||
path = "/join/%s" % urllib.quote(args["roomname"])
|
||||
reactor.callFromThread(self._run_and_pprint, "PUT", path, {})
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", path, {})
|
||||
except Exception as e:
|
||||
print e
|
||||
|
||||
@@ -360,14 +379,14 @@ class SynapseCmd(cmd.Cmd):
|
||||
def _do_invite(self, roomid, userstring):
|
||||
if (not userstring.startswith('@') and
|
||||
self._is_on("complete_usernames")):
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/lookup"
|
||||
url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup"
|
||||
|
||||
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
|
||||
|
||||
mxid = None
|
||||
|
||||
if 'mxid' in json_res and 'signatures' in json_res:
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/pubkey/ed25519"
|
||||
url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519"
|
||||
|
||||
pubKey = None
|
||||
pubKeyObj = yield self.http_client.do_request("GET", url)
|
||||
@@ -700,7 +719,7 @@ def main(server_url, identity_server_url, username, token, config_path):
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser("Starts a synapse client.")
|
||||
parser.add_argument(
|
||||
"-s", "--server", dest="server", default="http://localhost:8080",
|
||||
"-s", "--server", dest="server", default="http://localhost:8008",
|
||||
help="The URL of the home server to talk to.")
|
||||
parser.add_argument(
|
||||
"-i", "--identity-server", dest="identityserver", default="http://localhost:8090",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
21
database-prepare-for-0.5.0.sh
Executable file
21
database-prepare-for-0.5.0.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This is will prepare a synapse database for running with v0.5.0 of synapse.
|
||||
# It will store all the user information, but will *delete* all messages and
|
||||
# room data.
|
||||
|
||||
set -e
|
||||
|
||||
cp "$1" "$1.bak"
|
||||
|
||||
DUMP=$(sqlite3 "$1" << 'EOF'
|
||||
.dump users
|
||||
.dump access_tokens
|
||||
.dump presence
|
||||
.dump profiles
|
||||
EOF
|
||||
)
|
||||
|
||||
rm "$1"
|
||||
|
||||
sqlite3 "$1" <<< "$DUMP"
|
||||
@@ -14,3 +14,4 @@ fi
|
||||
find "$DIR" -name "*.log" -delete
|
||||
find "$DIR" -name "*.db" -delete
|
||||
|
||||
rm -rf $DIR/etc
|
||||
|
||||
9
demo/demo.tls.dh
Normal file
9
demo/demo.tls.dh
Normal file
@@ -0,0 +1,9 @@
|
||||
2048-bit DH parameters taken from rfc3526
|
||||
-----BEGIN DH PARAMETERS-----
|
||||
MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb
|
||||
IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft
|
||||
awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT
|
||||
mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh
|
||||
fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq
|
||||
5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==
|
||||
-----END DH PARAMETERS-----
|
||||
@@ -6,20 +6,38 @@ CWD=$(pwd)
|
||||
|
||||
cd "$DIR/.."
|
||||
|
||||
mkdir -p demo/etc
|
||||
|
||||
# Check the --no-rate-limit param
|
||||
PARAMS=""
|
||||
if [ $# -eq 1 ]; then
|
||||
if [ $1 = "--no-rate-limit" ]; then
|
||||
PARAMS="--rc-messages-per-second 1000 --rc-message-burst-count 1000"
|
||||
fi
|
||||
fi
|
||||
|
||||
for port in 8080 8081 8082; do
|
||||
echo "Starting server on port $port... "
|
||||
|
||||
https_port=$((port + 400))
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
-p "$port" \
|
||||
-H "localhost:$port" \
|
||||
--generate-config \
|
||||
--config-path "demo/etc/$port.config" \
|
||||
-p "$https_port" \
|
||||
--unsecure-port "$port" \
|
||||
-H "localhost:$https_port" \
|
||||
-f "$DIR/$port.log" \
|
||||
-d "$DIR/$port.db" \
|
||||
-vv \
|
||||
-D --pid-file "$DIR/$port.pid" \
|
||||
--manhole $((port + 1000))
|
||||
--manhole $((port + 1000)) \
|
||||
--tls-dh-params-path "demo/demo.tls.dh" \
|
||||
$PARAMS $SYNAPSE_PARAMS
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
--config-path "demo/etc/$port.config" \
|
||||
-vv \
|
||||
|
||||
done
|
||||
|
||||
echo "Starting webclient on port 8000..."
|
||||
python "demo/webserver.py" -p 8000 -P "$DIR/webserver.pid" "webclient"
|
||||
|
||||
cd "$CWD"
|
||||
|
||||
6
docs/README.rst
Normal file
6
docs/README.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
All matrix-generic documentation now lives in its own project at
|
||||
|
||||
github.com/matrix-org/matrix-doc.git
|
||||
|
||||
Only Synapse implementation-specific documentation lives here now
|
||||
(together with some older stuff will be shortly migrated over to matrix-doc)
|
||||
@@ -1,3 +1,9 @@
|
||||
.. WARNING::
|
||||
These architecture notes are spectacularly old, and date back to when Synapse
|
||||
was just federation code in isolation. This should be merged into the main
|
||||
spec.
|
||||
|
||||
|
||||
= Server to Server =
|
||||
|
||||
== Server to Server Stack ==
|
||||
68
docs/architecture.rst
Normal file
68
docs/architecture.rst
Normal file
@@ -0,0 +1,68 @@
|
||||
Synapse Architecture
|
||||
====================
|
||||
|
||||
As of the end of Oct 2014, Synapse's overall architecture looks like::
|
||||
|
||||
synapse
|
||||
.-----------------------------------------------------.
|
||||
| Notifier |
|
||||
| ^ | |
|
||||
| | | |
|
||||
| .------------|------. |
|
||||
| | handlers/ | | |
|
||||
| | v | |
|
||||
| | Event*Handler <--------> rest/* <=> Client
|
||||
| | Rooms*Handler | |
|
||||
HSes <=> federation/* <==> FederationHandler | |
|
||||
| | | PresenceHandler | |
|
||||
| | | TypingHandler | |
|
||||
| | '-------------------' |
|
||||
| | | | |
|
||||
| | state/* | |
|
||||
| | | | |
|
||||
| | v v |
|
||||
| `--------------> storage/* |
|
||||
| | |
|
||||
'--------------------------|--------------------------'
|
||||
v
|
||||
.----.
|
||||
| DB |
|
||||
'----'
|
||||
|
||||
* Handlers: business logic of synapse itself. Follows a set contract of BaseHandler:
|
||||
|
||||
- BaseHandler gives us onNewRoomEvent which: (TODO: flesh this out and make it less cryptic):
|
||||
|
||||
+ handle_state(event)
|
||||
+ auth(event)
|
||||
+ persist_event(event)
|
||||
+ notify notifier or federation(event)
|
||||
|
||||
- PresenceHandler: use distributor to get EDUs out of Federation. Very
|
||||
lightweight logic built on the distributor
|
||||
- TypingHandler: use distributor to get EDUs out of Federation. Very
|
||||
lightweight logic built on the distributor
|
||||
- EventsHandler: handles the events stream...
|
||||
- FederationHandler: - gets PDU from Federation Layer; turns into an event;
|
||||
follows basehandler functionality.
|
||||
- RoomsHandler: does all the room logic, including members - lots of classes in
|
||||
RoomsHandler.
|
||||
- ProfileHandler: talks to the storage to store/retrieve profile info.
|
||||
|
||||
* EventFactory: generates events of particular event types.
|
||||
* Notifier: Backs the events handler
|
||||
* REST: Interfaces handlers and events to the outside world via HTTP/JSON.
|
||||
Converts events back and forth from JSON.
|
||||
* Federation: holds the HTTP client & server to talk to other servers. Does
|
||||
replication to make sure there's nothing missing in the graph. Handles
|
||||
reliability. Handles txns.
|
||||
* Distributor: generic event bus. used for presence & typing only currently.
|
||||
Notifier could be implemented using Distributor - so far we are only using for
|
||||
things which actually /require/ dynamic pluggability however as it can
|
||||
obfuscate the actual flow of control.
|
||||
* Auth: helper singleton to say whether a given event is allowed to do a given
|
||||
thing (TODO: put this on the diagram)
|
||||
* State: helper singleton: does state conflict resolution. You give it an event
|
||||
and it tells you if it actually updates the state or not, and annotates the
|
||||
event up properly and handles merge conflict resolution.
|
||||
* Storage: abstracts the storage engine.
|
||||
@@ -1,303 +0,0 @@
|
||||
TODO(kegan): Tweak joinalias API keys/path? Event stream historical > live needs
|
||||
a token (currently doesn't). im/sync responses include outdated event formats
|
||||
(room membership change messages). Room config (specifically: message history,
|
||||
public rooms). /register seems super simplistic compared to /login, maybe it
|
||||
would be better if /register used the same technique as /login? /register should
|
||||
be "user" not "user_id".
|
||||
|
||||
|
||||
How to use the client-server API
|
||||
================================
|
||||
|
||||
This guide focuses on how the client-server APIs *provided by the reference
|
||||
home server* can be used. Since this is specific to a home server
|
||||
implementation, there may be variations in relation to registering/logging in
|
||||
which are not covered in extensive detail in this guide.
|
||||
|
||||
If you haven't already, get a home server up and running on
|
||||
``http://localhost:8080``.
|
||||
|
||||
|
||||
Accounts
|
||||
========
|
||||
Before you can send and receive messages, you must **register** for an account.
|
||||
If you already have an account, you must **login** into it.
|
||||
|
||||
**Try out the fiddle: http://jsfiddle.net/jrf1h02d/**
|
||||
|
||||
Registration
|
||||
------------
|
||||
The aim of registration is to get a user ID and access token which you will need
|
||||
when accessing other APIs::
|
||||
|
||||
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register"
|
||||
|
||||
{
|
||||
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
|
||||
"home_server": "localhost",
|
||||
"user_id": "@example:localhost"
|
||||
}
|
||||
|
||||
NB: If a ``user_id`` is not specified, one will be randomly generated for you.
|
||||
If you do not specify a ``password``, you will be unable to login to the account
|
||||
if you forget the ``access_token``.
|
||||
|
||||
Implementation note: The matrix specification does not enforce how users
|
||||
register with a server. It just specifies the URL path and absolute minimum
|
||||
keys. The reference home server uses a username/password to authenticate user,
|
||||
but other home servers may use different methods.
|
||||
|
||||
Login
|
||||
-----
|
||||
The aim when logging in is to get an access token for your existing user ID::
|
||||
|
||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/login"
|
||||
|
||||
{
|
||||
"type": "m.login.password"
|
||||
}
|
||||
|
||||
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login"
|
||||
|
||||
{
|
||||
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
|
||||
"home_server": "localhost",
|
||||
"user_id": "@example:localhost"
|
||||
}
|
||||
|
||||
Implementation note: Different home servers may implement different methods for
|
||||
logging in to an existing account. In order to check that you know how to login
|
||||
to this home server, you must perform a ``GET`` first and make sure you
|
||||
recognise the login type. If you do not know how to login, you can
|
||||
``GET /login/fallback`` which will return a basic webpage which you can use to
|
||||
login. The reference home server implementation support username/password login,
|
||||
but other home servers may support different login methods (e.g. OAuth2).
|
||||
|
||||
|
||||
Communicating
|
||||
=============
|
||||
|
||||
In order to communicate with another user, you must **create a room** with that
|
||||
user and **send a message** to that room.
|
||||
|
||||
**Try out the fiddle: http://jsfiddle.net/jnwqcshc/**
|
||||
|
||||
Creating a room
|
||||
---------------
|
||||
If you want to send a message to someone, you have to be in a room with them. To
|
||||
create a room::
|
||||
|
||||
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
||||
|
||||
{
|
||||
"room_alias": "#tutorial:localhost",
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
||||
}
|
||||
|
||||
The "room alias" is a human-readable string which can be shared with other users
|
||||
so they can join a room, rather than the room ID which is a randomly generated
|
||||
string. You can have multiple room aliases per room.
|
||||
|
||||
TODO(kegan): How to add/remove aliases from an existing room.
|
||||
|
||||
|
||||
Sending messages
|
||||
----------------
|
||||
You can now send messages to this room::
|
||||
|
||||
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
||||
|
||||
NB: There are no limitations to the types of messages which can be exchanged.
|
||||
The only requirement is that ``"msgtype"`` is specified.
|
||||
|
||||
NB: Depending on the room config, users who join the room may be able to see
|
||||
message history from before they joined.
|
||||
|
||||
Users and rooms
|
||||
===============
|
||||
|
||||
Each room can be configured to allow or disallow certain rules. In particular,
|
||||
these rules may specify if you require an **invitation** from someone already in
|
||||
the room in order to **join the room**. In addition, you may also be able to
|
||||
join a room **via a room alias** if one was set up.
|
||||
|
||||
**Try out the fiddle: http://jsfiddle.net/og1xokcr/**
|
||||
|
||||
Inviting a user to a room
|
||||
-------------------------
|
||||
You can directly invite a user to a room like so::
|
||||
|
||||
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
||||
|
||||
This informs ``@myfriend:localhost`` of the room ID
|
||||
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
|
||||
|
||||
Joining a room via an invite
|
||||
----------------------------
|
||||
If you receive an invite, you can join the room by changing the membership to
|
||||
join::
|
||||
|
||||
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
||||
|
||||
NB: Only the person invited (``@myfriend:localhost``) can change the membership
|
||||
state to ``"join"``.
|
||||
|
||||
Joining a room via an alias
|
||||
---------------------------
|
||||
Alternatively, if you know the room alias for this room and the room config
|
||||
allows it, you can directly join a room via the alias::
|
||||
|
||||
curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
||||
|
||||
{
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
||||
}
|
||||
|
||||
You will need to use the room ID when sending messages, not the room alias.
|
||||
|
||||
NB: If the room is configured to be an invite-only room, you will still require
|
||||
an invite in order to join the room even though you know the room alias. As a
|
||||
result, it is more common to see a room alias in relation to a public room,
|
||||
which do not require invitations.
|
||||
|
||||
Getting events
|
||||
==============
|
||||
An event is some interesting piece of data that a client may be interested in.
|
||||
It can be a message in a room, a room invite, etc. There are many different ways
|
||||
of getting events, depending on what the client already knows.
|
||||
|
||||
**Try out the fiddle: http://jsfiddle.net/5uk4dqe2/**
|
||||
|
||||
Getting all state
|
||||
-----------------
|
||||
If the client doesn't know any information on the rooms the user is
|
||||
invited/joined on, they can get all the user's state for all rooms::
|
||||
|
||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
||||
|
||||
[
|
||||
{
|
||||
"membership": "join",
|
||||
"messages": {
|
||||
"chunk": [
|
||||
{
|
||||
"content": {
|
||||
"body": "@example:localhost joined the room.",
|
||||
"hsob_ts": 1408444664249,
|
||||
"membership": "join",
|
||||
"membership_source": "@example:localhost",
|
||||
"membership_target": "@example:localhost",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "lZjmmlrEvo",
|
||||
"msg_id": "m1408444664249",
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
||||
"type": "m.room.message",
|
||||
"user_id": "_homeserver_"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "hello",
|
||||
"hsob_ts": 1408445405672,
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "BiBJqamISg",
|
||||
"msg_id": "msgid1",
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
||||
"type": "m.room.message",
|
||||
"user_id": "@example:localhost"
|
||||
},
|
||||
[...]
|
||||
{
|
||||
"content": {
|
||||
"body": "@myfriend:localhost joined the room.",
|
||||
"hsob_ts": 1408446501661,
|
||||
"membership": "join",
|
||||
"membership_source": "@myfriend:localhost",
|
||||
"membership_target": "@myfriend:localhost",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "IMmXbOzFAa",
|
||||
"msg_id": "m1408446501661",
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
||||
"type": "m.room.message",
|
||||
"user_id": "_homeserver_"
|
||||
}
|
||||
],
|
||||
"end": "20",
|
||||
"start": "0"
|
||||
},
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
||||
}
|
||||
]
|
||||
|
||||
This returns all the room IDs of rooms the user is invited/joined on, as well as
|
||||
all of the messages and feedback for these rooms. This can be a LOT of data. You
|
||||
may just want the most recent message for each room. This can be achieved by
|
||||
applying pagination stream parameters to this request::
|
||||
|
||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
|
||||
|
||||
[
|
||||
{
|
||||
"membership": "join",
|
||||
"messages": {
|
||||
"chunk": [
|
||||
{
|
||||
"content": {
|
||||
"body": "@myfriend:localhost joined the room.",
|
||||
"hsob_ts": 1408446501661,
|
||||
"membership": "join",
|
||||
"membership_source": "@myfriend:localhost",
|
||||
"membership_target": "@myfriend:localhost",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "IMmXbOzFAa",
|
||||
"msg_id": "m1408446501661",
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
||||
"type": "m.room.message",
|
||||
"user_id": "_homeserver_"
|
||||
}
|
||||
],
|
||||
"end": "20",
|
||||
"start": "21"
|
||||
},
|
||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
||||
}
|
||||
]
|
||||
|
||||
Getting live state
|
||||
------------------
|
||||
Once you know which rooms the client has previously interacted with, you need to
|
||||
listen for incoming events. This can be done like so::
|
||||
|
||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
|
||||
|
||||
{
|
||||
"chunk": [],
|
||||
"end": "215",
|
||||
"start": "215"
|
||||
}
|
||||
|
||||
This will block waiting for an incoming event, timing out after several seconds.
|
||||
Even if there are no new events (as in the example above), there will be some
|
||||
pagination stream response keys. The client should make subsequent requests
|
||||
using the value of the ``"end"`` key (in this case ``215``) as the ``from``
|
||||
query parameter. This value should be stored so when the client reopens your app
|
||||
after a period of inactivity, you can resume from where you got up to in the
|
||||
event stream. If it has been a long period of inactivity, there may be LOTS of
|
||||
events waiting for the user. In this case, you may wish to get all state instead
|
||||
and then resume getting live state from a newer end token.
|
||||
|
||||
NB: The timeout can be changed by adding a ``timeout`` query parameter, which is
|
||||
in milliseconds. A timeout of 0 will not block.
|
||||
|
||||
|
||||
Example application
|
||||
-------------------
|
||||
The following example demonstrates registration and login, live event streaming,
|
||||
creating and joining rooms, sending messages, getting member lists and getting
|
||||
historical messages for a room. This covers most functionality of a messaging
|
||||
application.
|
||||
|
||||
**Try out the fiddle: http://jsfiddle.net/L8r3o1wr/**
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"apis": [
|
||||
{
|
||||
"path": "/login",
|
||||
"description": "Login operations"
|
||||
},
|
||||
{
|
||||
"path": "/registration",
|
||||
"description": "Registration operations"
|
||||
},
|
||||
{
|
||||
"path": "/rooms",
|
||||
"description": "Room operations"
|
||||
},
|
||||
{
|
||||
"path": "/profile",
|
||||
"description": "Profile operations"
|
||||
},
|
||||
{
|
||||
"path": "/presence",
|
||||
"description": "Presence operations"
|
||||
},
|
||||
{
|
||||
"path": "/events",
|
||||
"description": "Event operations"
|
||||
},
|
||||
{
|
||||
"path": "/directory",
|
||||
"description": "Directory operations"
|
||||
}
|
||||
],
|
||||
"authorizations": {
|
||||
"token": {
|
||||
"scopes": []
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"title": "Matrix Client-Server API Reference",
|
||||
"description": "This contains the client-server API for the reference implementation of the home server",
|
||||
"termsOfServiceUrl": "http://matrix.org",
|
||||
"license": "Apache 2.0",
|
||||
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/directory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/directory/room/{roomAlias}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the room ID corresponding to this room alias.",
|
||||
"type": "DirectoryResponse",
|
||||
"nickname": "get_room_id_for_alias",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Create a new mapping from room alias to room ID.",
|
||||
"type": "void",
|
||||
"nickname": "add_room_alias",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The room ID to set.",
|
||||
"required": true,
|
||||
"type": "RoomAliasRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"DirectoryResponse": {
|
||||
"id": "DirectoryResponse",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The fully-qualified room ID.",
|
||||
"required": true
|
||||
},
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "string"
|
||||
},
|
||||
"description": "A list of servers that know about this room.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomAliasRequest": {
|
||||
"id": "RoomAliasRequest",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room ID to map the alias to.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/events",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/events",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Listen on the event stream",
|
||||
"notes": "This can only be done by the logged in user. This will block until an event is received, or until the timeout is reached.",
|
||||
"type": "PaginationChunk",
|
||||
"nickname": "get_event_stream"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "from",
|
||||
"description": "The token to stream from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"description": "The maximum time in milliseconds to wait for an event.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad pagination token."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/events/{eventId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get information about a single event.",
|
||||
"notes": "Get information about a single event.",
|
||||
"type": "Event",
|
||||
"nickname": "get_event",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventId",
|
||||
"description": "The event ID to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Event not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/initialSync",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get this user's current state.",
|
||||
"notes": "Get this user's current state.",
|
||||
"type": "InitialSyncResponse",
|
||||
"nickname": "initial_sync",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of messages to return for each room.",
|
||||
"type": "integer",
|
||||
"paramType": "query",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/publicRooms",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of publicly visible rooms.",
|
||||
"type": "PublicRoomsPaginationChunk",
|
||||
"nickname": "get_public_room_list"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"PaginationChunk": {
|
||||
"id": "PaginationChunk",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "An array of events.",
|
||||
"required": true,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"id": "Event",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicRoomInfo": {
|
||||
"id": "PublicRoomInfo",
|
||||
"properties": {
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"description": "A list of room aliases for this room.",
|
||||
"items": {
|
||||
"$ref": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the room, as given by the m.room.name state event."
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room ID for this public room.",
|
||||
"required": true
|
||||
},
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "The topic of this room, as given by the m.room.topic state event."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicRoomsPaginationChunk": {
|
||||
"id": "PublicRoomsPaginationChunk",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of public room data.",
|
||||
"required": true,
|
||||
"items": {
|
||||
"$ref": "PublicRoomInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncResponse": {
|
||||
"id": "InitialSyncResponse",
|
||||
"properties": {
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A streaming token which can be used with /events to continue from this snapshot of data.",
|
||||
"required": true
|
||||
},
|
||||
"presence": {
|
||||
"type": "array",
|
||||
"description": "A list of presence events.",
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
"rooms": {
|
||||
"type": "array",
|
||||
"description": "A list of initial sync room data.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "InitialSyncRoomData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncRoomData": {
|
||||
"id": "InitialSyncRoomData",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "This user's membership state in this room.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of this room.",
|
||||
"required": true
|
||||
},
|
||||
"messages": {
|
||||
"type": "PaginationChunk",
|
||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
||||
"required": false
|
||||
},
|
||||
"state": {
|
||||
"type": "array",
|
||||
"description": "A list of state events representing the current state of the room.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"apis": [
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"nickname": "get_login_info",
|
||||
"notes": "All login stages MUST be mentioned if there is >1 login type.",
|
||||
"summary": "Get the login mechanism to use when logging in.",
|
||||
"type": "LoginInfo"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"nickname": "submit_login",
|
||||
"notes": "If this is part of a multi-stage login, there MUST be a 'session' key.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A login submission",
|
||||
"name": "body",
|
||||
"paramType": "body",
|
||||
"required": true,
|
||||
"type": "LoginSubmission"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad login type"
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Missing JSON keys"
|
||||
}
|
||||
],
|
||||
"summary": "Submit a login action.",
|
||||
"type": "LoginResult"
|
||||
}
|
||||
],
|
||||
"path": "/login"
|
||||
}
|
||||
],
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"models": {
|
||||
"LoginInfo": {
|
||||
"id": "LoginInfo",
|
||||
"properties": {
|
||||
"stages": {
|
||||
"description": "Multi-stage login only: An array of all the login types required to login.",
|
||||
"format": "string",
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "The login type that must be used when logging in.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginResult": {
|
||||
"id": "LoginResult",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"description": "The access token for this user's login if this is the final stage of the login process.",
|
||||
"type": "string"
|
||||
},
|
||||
"next": {
|
||||
"description": "Multi-stage login only: The next login type to submit.",
|
||||
"type": "string"
|
||||
},
|
||||
"session": {
|
||||
"description": "Multi-stage login only: The session token to send when submitting the next login type.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginSubmission": {
|
||||
"id": "LoginSubmission",
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "The type of login being submitted.",
|
||||
"type": "string"
|
||||
},
|
||||
"session": {
|
||||
"description": "Multi-stage login only: The session token from an earlier login stage.",
|
||||
"type": "string"
|
||||
},
|
||||
"_login_type_defined_keys_": {
|
||||
"description": "Keys as defined by the specified login type, e.g. \"user\", \"password\""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"resourcePath": "/login",
|
||||
"swaggerVersion": "1.2"
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/presence",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/presence/{userId}/status",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Update this user's presence state.",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "update_presence",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new presence state",
|
||||
"required": true,
|
||||
"type": "PresenceUpdate",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get this user's presence state.",
|
||||
"notes": "Get this user's presence state.",
|
||||
"type": "PresenceUpdate",
|
||||
"nickname": "get_presence",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/presence/list/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Retrieve a list of presences for all of this user's friends.",
|
||||
"notes": "",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Presence"
|
||||
},
|
||||
"nickname": "get_presence_list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence list to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Add or remove users from this presence list.",
|
||||
"notes": "Add or remove users from this presence list.",
|
||||
"type": "void",
|
||||
"nickname": "modify_presence_list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence list is being modified.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The modifications to make to this presence list.",
|
||||
"required": true,
|
||||
"type": "PresenceListModifications",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"PresenceUpdate": {
|
||||
"id": "PresenceUpdate",
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Enum: The presence state.",
|
||||
"enum": [
|
||||
"offline",
|
||||
"unavailable",
|
||||
"online",
|
||||
"free_for_chat"
|
||||
]
|
||||
},
|
||||
"status_msg": {
|
||||
"type": "string",
|
||||
"description": "The user-defined message associated with this presence state."
|
||||
}
|
||||
},
|
||||
"subTypes": [
|
||||
"Presence"
|
||||
]
|
||||
},
|
||||
"Presence": {
|
||||
"id": "Presence",
|
||||
"properties": {
|
||||
"mtime_age": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The last time this user's presence state changed, in milliseconds."
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "The fully qualified user ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PresenceListModifications": {
|
||||
"id": "PresenceListModifications",
|
||||
"properties": {
|
||||
"invite": {
|
||||
"type": "array",
|
||||
"description": "A list of user IDs to add to the list.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A fully qualified user ID."
|
||||
}
|
||||
},
|
||||
"drop": {
|
||||
"type": "array",
|
||||
"description": "A list of user IDs to remove from the list.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A fully qualified user ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/profile",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/profile/{userId}/displayname",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Set a display name.",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "set_display_name",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new display name for this user.",
|
||||
"required": true,
|
||||
"type": "DisplayName",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose display name to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a display name.",
|
||||
"notes": "This can be done by anyone.",
|
||||
"type": "DisplayName",
|
||||
"nickname": "get_display_name",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose display name to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/profile/{userId}/avatar_url",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Set an avatar URL.",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "set_avatar_url",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new avatar url for this user.",
|
||||
"required": true,
|
||||
"type": "AvatarUrl",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose avatar url to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get an avatar url.",
|
||||
"notes": "This can be done by anyone.",
|
||||
"type": "AvatarUrl",
|
||||
"nickname": "get_avatar_url",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose avatar url to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"DisplayName": {
|
||||
"id": "DisplayName",
|
||||
"properties": {
|
||||
"displayname": {
|
||||
"type": "string",
|
||||
"description": "The textual display name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AvatarUrl": {
|
||||
"id": "AvatarUrl",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"description": "A url to an image representing an avatar."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"apis": [
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"nickname": "register",
|
||||
"notes": "Volatile: This API is likely to change.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A registration request",
|
||||
"name": "body",
|
||||
"paramType": "body",
|
||||
"required": true,
|
||||
"type": "RegistrationRequest"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "No JSON object."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "User ID must only contain characters which do not require url encoding."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "User ID already taken."
|
||||
}
|
||||
],
|
||||
"summary": "Register with the home server.",
|
||||
"type": "RegistrationResponse"
|
||||
}
|
||||
],
|
||||
"path": "/register"
|
||||
}
|
||||
],
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"models": {
|
||||
"RegistrationResponse": {
|
||||
"id": "RegistrationResponse",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"description": "The access token for this user.",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"description": "The fully-qualified user ID.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RegistrationRequest": {
|
||||
"id": "RegistrationRequest",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"description": "The desired user ID. If not specified, a random user ID will be allocated.",
|
||||
"type": "string",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"resourcePath": "/register",
|
||||
"swaggerVersion": "1.2"
|
||||
}
|
||||
|
||||
@@ -1,863 +0,0 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/rooms",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"authorizations": {
|
||||
"token": []
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a generic non-state event to this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}",
|
||||
"type": "EventId",
|
||||
"nickname": "send_non_state_event",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The event contents",
|
||||
"required": true,
|
||||
"type": "EventContent",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "eventType",
|
||||
"description": "The type of event to send.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/{eventType}/{stateKey}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a generic state event to this room.",
|
||||
"notes": "The state key can be omitted, such that you can PUT to /rooms/{roomId}/state/{eventType}. The state key defaults to a 0 length string in this case.",
|
||||
"type": "void",
|
||||
"nickname": "send_state_event",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The event contents",
|
||||
"required": true,
|
||||
"type": "EventContent",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "eventType",
|
||||
"description": "The type of event to send.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "stateKey",
|
||||
"description": "An identifier used to specify clobbering semantics. State events with the same (roomId, eventType, stateKey) will be replaced.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a message in this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
|
||||
"type": "EventId",
|
||||
"nickname": "send_message",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The message contents",
|
||||
"required": true,
|
||||
"type": "Message",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/m.room.topic",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Set the topic for this room.",
|
||||
"notes": "Set the topic for this room.",
|
||||
"type": "void",
|
||||
"nickname": "set_topic",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The topic contents",
|
||||
"required": true,
|
||||
"type": "Topic",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to set the topic in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the topic for this room.",
|
||||
"notes": "Get the topic for this room.",
|
||||
"type": "Topic",
|
||||
"nickname": "get_topic",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get topic in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Topic not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send feedback to a message.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
|
||||
"type": "EventId",
|
||||
"nickname": "send_feedback",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The feedback contents",
|
||||
"required": true,
|
||||
"type": "Feedback",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the feedback in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad feedback type."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/invite/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Invite a user to this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
|
||||
"type": "void",
|
||||
"nickname": "invite",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The user to invite.",
|
||||
"required": true,
|
||||
"type": "InviteRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/join/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
|
||||
"type": "void",
|
||||
"nickname": "join_room",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/leave/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Leave this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
|
||||
"type": "void",
|
||||
"nickname": "leave",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to leave.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/m.room.member/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Change the membership state for a user in a room.",
|
||||
"notes": "Change the membership state for a user in a room.",
|
||||
"type": "void",
|
||||
"nickname": "set_membership",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new membership state",
|
||||
"required": true,
|
||||
"type": "Member",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose membership is being changed.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "No membership key."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad membership value."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When inviting: You are not in the room."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When inviting: <target> is already in the room."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When joining: Cannot force another user to join."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When joining: You are not invited to this room."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the membership state of a user in a room.",
|
||||
"notes": "Get the membership state of a user in a room.",
|
||||
"type": "Member",
|
||||
"nickname": "get_membership",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose membership state you want to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Member not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/join/{roomAliasOrId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join a room via a room alias or room ID.",
|
||||
"notes": "Join a room via a room alias or room ID.",
|
||||
"type": "RoomInfo",
|
||||
"nickname": "join",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAliasOrId",
|
||||
"description": "The room alias or room ID to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad room alias."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/createRoom",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Create a room.",
|
||||
"notes": "Create a room.",
|
||||
"type": "RoomInfo",
|
||||
"nickname": "create_room",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The desired configuration for the room.",
|
||||
"required": true,
|
||||
"type": "RoomConfig",
|
||||
"paramType": "body"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Body must be JSON."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Room alias already taken."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of messages for this room.",
|
||||
"notes": "Get a list of messages for this room.",
|
||||
"type": "MessagePaginationChunk",
|
||||
"nickname": "get_messages",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get messages in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "from",
|
||||
"description": "The token to start getting results from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"description": "The token to stop getting results at.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of messages to return.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/members",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of members for this room.",
|
||||
"notes": "Get a list of members for this room.",
|
||||
"type": "MemberPaginationChunk",
|
||||
"nickname": "get_members",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get a list of members from.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "from",
|
||||
"description": "The token to start getting results from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"description": "The token to stop getting results at.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of members to return.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of all the current state events for this room.",
|
||||
"notes": "Get a list of all the current state events for this room.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
},
|
||||
"nickname": "get_state_events",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get a list of current state events from.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/initialSync",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get all the current information for this room, including messages and state events.",
|
||||
"notes": "Get all the current information for this room, including messages and state events.",
|
||||
"type": "InitialSyncRoomData",
|
||||
"nickname": "get_room_sync_data",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get information for.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"Topic": {
|
||||
"id": "Topic",
|
||||
"properties": {
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "The topic text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Message": {
|
||||
"id": "Message",
|
||||
"properties": {
|
||||
"msgtype": {
|
||||
"type": "string",
|
||||
"description": "The type of message being sent, e.g. \"m.text\"",
|
||||
"required": true
|
||||
},
|
||||
"_msgtype_defined_keys_": {
|
||||
"description": "Additional keys as defined by the msgtype, e.g. \"body\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"Feedback": {
|
||||
"id": "Feedback",
|
||||
"properties": {
|
||||
}
|
||||
},
|
||||
"Member": {
|
||||
"id": "Member",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "Enum: The membership state of this member.",
|
||||
"enum": [
|
||||
"invite",
|
||||
"join",
|
||||
"leave",
|
||||
"knock"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomInfo": {
|
||||
"id": "RoomInfo",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The allocated room ID.",
|
||||
"required": true
|
||||
},
|
||||
"room_alias": {
|
||||
"type": "string",
|
||||
"description": "The alias for the room.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomConfig": {
|
||||
"id": "RoomConfig",
|
||||
"properties": {
|
||||
"visibility": {
|
||||
"type": "string",
|
||||
"description": "Enum: The room visibility.",
|
||||
"required": false,
|
||||
"enum": [
|
||||
"public",
|
||||
"private"
|
||||
]
|
||||
},
|
||||
"room_alias_name": {
|
||||
"type": "string",
|
||||
"description": "The alias to give the new room.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"PaginationRequest": {
|
||||
"id": "PaginationRequest",
|
||||
"properties": {
|
||||
"from": {
|
||||
"type": "string",
|
||||
"description": "The token to start getting results from."
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"description": "The token to stop getting results at."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "The maximum number of entries to return."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PaginationChunk": {
|
||||
"id": "PaginationChunk",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"subTypes": [
|
||||
"MessagePaginationChunk"
|
||||
]
|
||||
},
|
||||
"MessagePaginationChunk": {
|
||||
"id": "MessagePaginationChunk",
|
||||
"properties": {
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of message events.",
|
||||
"items": {
|
||||
"$ref": "MessageEvent"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"MemberPaginationChunk": {
|
||||
"id": "MemberPaginationChunk",
|
||||
"properties": {
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of member events.",
|
||||
"items": {
|
||||
"$ref": "MemberEvent"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"id": "Event",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event. This is automatically set by the server.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred. This is automatically set by the server.",
|
||||
"required": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The event type.",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"subTypes": [
|
||||
"MessageEvent"
|
||||
]
|
||||
},
|
||||
"EventId": {
|
||||
"id": "EventId",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "The allocated event ID for this event.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EventContent": {
|
||||
"id": "EventContent",
|
||||
"properties": {
|
||||
"__event_content_keys__": {
|
||||
"type": "string",
|
||||
"description": "Event-specific content keys and values.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageEvent": {
|
||||
"id": "MessageEvent",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "Message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MemberEvent": {
|
||||
"id": "MemberEvent",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "Member"
|
||||
}
|
||||
}
|
||||
},
|
||||
"InviteRequest": {
|
||||
"id": "InviteRequest",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "The fully-qualified user ID."
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncRoomData": {
|
||||
"id": "InitialSyncRoomData",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "This user's membership state in this room.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of this room.",
|
||||
"required": true
|
||||
},
|
||||
"messages": {
|
||||
"type": "MessagePaginationChunk",
|
||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
||||
"required": false
|
||||
},
|
||||
"state": {
|
||||
"type": "array",
|
||||
"description": "A list of state events representing the current state of the room.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
===================
|
||||
Documentation Style
|
||||
===================
|
||||
|
||||
A brief single sentence to describe what this file contains; in this case a
|
||||
description of the style to write documentation in.
|
||||
|
||||
|
||||
Sections
|
||||
========
|
||||
|
||||
Each section should be separated from the others by two blank lines. Headings
|
||||
should be underlined using a row of equals signs (===). Paragraphs should be
|
||||
separated by a single blank line, and wrap to no further than 80 columns.
|
||||
|
||||
[[TODO(username): if you want to leave some unanswered questions, notes for
|
||||
further consideration, or other kinds of comment, use a TODO section. Make sure
|
||||
to notate it with your name so we know who to ask about it!]]
|
||||
|
||||
Subsections
|
||||
-----------
|
||||
|
||||
If required, subsections can use a row of dashes to underline their header. A
|
||||
single blank line between subsections of a single section.
|
||||
|
||||
|
||||
Bullet Lists
|
||||
============
|
||||
|
||||
* Bullet lists can use asterisks with a single space either side.
|
||||
|
||||
* Another blank line between list elements.
|
||||
|
||||
|
||||
Definition Lists
|
||||
================
|
||||
|
||||
Terms:
|
||||
Start in the first column, ending with a colon
|
||||
|
||||
Definitions:
|
||||
Take a two space indent, following immediately from the term without a blank
|
||||
line before it, but having a blank line afterwards.
|
||||
@@ -1,249 +0,0 @@
|
||||
========
|
||||
Presence
|
||||
========
|
||||
|
||||
A description of presence information and visibility between users.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Each user has the concept of Presence information. This encodes a sense of the
|
||||
"availability" of that user, suitable for display on other user's clients.
|
||||
|
||||
|
||||
Presence Information
|
||||
====================
|
||||
|
||||
The basic piece of presence information is an enumeration of a small set of
|
||||
state; such as "free to chat", "online", "busy", or "offline". The default state
|
||||
unless the user changes it is "online". Lower states suggest some amount of
|
||||
decreased availability from normal, which might have some client-side effect
|
||||
like muting notification sounds and suggests to other users not to bother them
|
||||
unless it is urgent. Equally, the "free to chat" state exists to let the user
|
||||
announce their general willingness to receive messages moreso than default.
|
||||
|
||||
Home servers should also allow a user to set their state as "hidden" - a state
|
||||
which behaves as offline, but allows the user to see the client state anyway and
|
||||
generally interact with client features such as reading message history or
|
||||
accessing contacts in the address book.
|
||||
|
||||
This basic state field applies to the user as a whole, regardless of how many
|
||||
client devices they have connected. The home server should synchronise this
|
||||
status choice among multiple devices to ensure the user gets a consistent
|
||||
experience.
|
||||
|
||||
Idle Time
|
||||
---------
|
||||
|
||||
As well as the basic state field, the presence information can also show a sense
|
||||
of an "idle timer". This should be maintained individually by the user's
|
||||
clients, and the homeserver can take the highest reported time as that to
|
||||
report. Likely this should be presented in fairly coarse granularity; possibly
|
||||
being limited to letting the home server automatically switch from a "free to
|
||||
chat" or "online" mode into "idle".
|
||||
|
||||
When a user is offline, the Home Server can still report when the user was last
|
||||
seen online, again perhaps in a somewhat coarse manner.
|
||||
|
||||
Device Type
|
||||
-----------
|
||||
|
||||
Client devices that may limit the user experience somewhat (such as "mobile"
|
||||
devices with limited ability to type on a real keyboard or read large amounts of
|
||||
text) should report this to the home server, as this is also useful information
|
||||
to report as "presence" if the user cannot be expected to provide a good typed
|
||||
response to messages.
|
||||
|
||||
|
||||
Presence List
|
||||
=============
|
||||
|
||||
Each user's home server stores a "presence list" for that user. This stores a
|
||||
list of other user IDs the user has chosen to add to it (remembering any ACL
|
||||
Pointer if appropriate).
|
||||
|
||||
To be added to a contact list, the user being added must grant permission. Once
|
||||
granted, both user's HS(es) store this information, as it allows the user who
|
||||
has added the contact some more abilities; see below. Since such subscriptions
|
||||
are likely to be bidirectional, HSes may wish to automatically accept requests
|
||||
when a reverse subscription already exists.
|
||||
|
||||
As a convenience, presence lists should support the ability to collect users
|
||||
into groups, which could allow things like inviting the entire group to a new
|
||||
("ad-hoc") chat room, or easy interaction with the profile information ACL
|
||||
implementation of the HS.
|
||||
|
||||
|
||||
Presence and Permissions
|
||||
========================
|
||||
|
||||
For a viewing user to be allowed to see the presence information of a target
|
||||
user, either
|
||||
|
||||
* The target user has allowed the viewing user to add them to their presence
|
||||
list, or
|
||||
|
||||
* The two users share at least one room in common
|
||||
|
||||
In the latter case, this allows for clients to display some minimal sense of
|
||||
presence information in a user list for a room.
|
||||
|
||||
Home servers can also use the user's choice of presence state as a signal for
|
||||
how to handle new private one-to-one chat message requests. For example, it
|
||||
might decide:
|
||||
|
||||
"free to chat": accept anything
|
||||
"online": accept from anyone in my addres book list
|
||||
"busy": accept from anyone in this "important people" group in my address
|
||||
book list
|
||||
|
||||
|
||||
API Efficiency
|
||||
==============
|
||||
|
||||
A simple implementation of presence messaging has the ability to cause a large
|
||||
amount of Internet traffic relating to presence updates. In order to minimise
|
||||
the impact of such a feature, the following observations can be made:
|
||||
|
||||
* There is no point in a Home Server polling status for peers in a user's
|
||||
presence list if the user has no clients connected that care about it.
|
||||
|
||||
* It is highly likely that most presence subscriptions will be symmetric - a
|
||||
given user watching another is likely to in turn be watched by that user.
|
||||
|
||||
* It is likely that most subscription pairings will be between users who share
|
||||
at least one Room in common, and so their Home Servers are actively
|
||||
exchanging message PDUs or transactions relating to that Room.
|
||||
|
||||
* Presence update messages do not need realtime guarantees. It is acceptable to
|
||||
delay delivery of updates for some small amount of time (10 seconds to a
|
||||
minute).
|
||||
|
||||
The general model of presence information is that of a HS registering its
|
||||
interest in receiving presence status updates from other HSes, which then
|
||||
promise to send them when required. Rather than actively polling for the
|
||||
currentt state all the time, HSes can rely on their relative stability to only
|
||||
push updates when required.
|
||||
|
||||
A Home Server should not rely on the longterm validity of this presence
|
||||
information, however, as this would not cover such cases as a user's server
|
||||
crashing and thus failing to inform their peers that users it used to host are
|
||||
no longer available online. Therefore, each promise of future updates should
|
||||
carry with a timeout value (whether explicit in the message, or implicit as some
|
||||
defined default in the protocol), after which the receiving HS should consider
|
||||
the information potentially stale and request it again.
|
||||
|
||||
However, because of the likelyhood that two home servers are exchanging messages
|
||||
relating to chat traffic in a room common to both of them, the ongoing receipt
|
||||
of these messages can be taken by each server as an implicit notification that
|
||||
the sending server is still up and running, and therefore that no status changes
|
||||
have happened; because if they had the server would have sent them. A second,
|
||||
larger timeout should be applied to this implicit inference however, to protect
|
||||
against implementation bugs or other reasons that the presence state cache may
|
||||
become invalid; eventually the HS should re-enquire the current state of users
|
||||
and update them with its own.
|
||||
|
||||
The following workflows can therefore be used to handle presence updates:
|
||||
|
||||
1 When a user first appears online their HS sends a message to each other HS
|
||||
containing at least one user to be watched; each message carrying both a
|
||||
notification of the sender's new online status, and a request to obtain and
|
||||
watch the target users' presence information. This message implicitly
|
||||
promises the sending HS will now push updates to the target HSes.
|
||||
|
||||
2 The target HSes then respond a single message each, containing the current
|
||||
status of the requested user(s). These messages too implicitly promise the
|
||||
target HSes will themselves push updates to the sending HS.
|
||||
|
||||
As these messages arrive at the sending user's HS they can be pushed to the
|
||||
user's client(s), possibly batched again to ensure not too many small
|
||||
messages which add extra protocol overheads.
|
||||
|
||||
At this point, all the user's clients now have the current presence status
|
||||
information for this moment in time, and have promised to send each other
|
||||
updates in future.
|
||||
|
||||
3 The HS maintains two watchdog timers per peer HS it is exchanging presence
|
||||
information with. The first timer should have a relatively small expiry
|
||||
(perhaps 1 minute), and the second timer should have a much longer time
|
||||
(perhaps 1 hour).
|
||||
|
||||
4 Any time any kind of message is received from a peer HS, the short-term
|
||||
presence timer associated with it is reset.
|
||||
|
||||
5 Whenever either of these timers expires, an HS should push a status reminder
|
||||
to the target HS whose timer has now expired, and request again from that
|
||||
server the status of the subscribed users.
|
||||
|
||||
6 On receipt of one of these presence status reminders, an HS can reset both
|
||||
of its presence watchdog timers.
|
||||
|
||||
To avoid bursts of traffic, implementations should attempt to stagger the expiry
|
||||
of the longer-term watchdog timers for different peer HSes.
|
||||
|
||||
When individual users actively change their status (either by explicit requests
|
||||
from clients, or inferred changes due to idle timers or client timeouts), the HS
|
||||
should batch up any status changes for some reasonable amount of time (10
|
||||
seconds to a minute). This allows for reduced protocol overheads in the case of
|
||||
multiple messages needing to be sent to the same peer HS; as is the likely
|
||||
scenario in many cases, such as a given human user having multiple user
|
||||
accounts.
|
||||
|
||||
|
||||
API Requirements
|
||||
================
|
||||
|
||||
The data model presented here puts the following requirements on the APIs:
|
||||
|
||||
Client-Server
|
||||
-------------
|
||||
|
||||
Requests that a client can make to its Home Server
|
||||
|
||||
* get/set current presence state
|
||||
Basic enumeration + ability to set a custom piece of text
|
||||
|
||||
* report per-device idle time
|
||||
After some (configurable?) idle time the device should send a single message
|
||||
to set the idle duration. The HS can then infer a "start of idle" instant and
|
||||
use that to keep the device idleness up to date. At some later point the
|
||||
device can cancel this idleness.
|
||||
|
||||
* report per-device type
|
||||
Inform the server that this device is a "mobile" device, or perhaps some
|
||||
other to-be-defined category of reduced capability that could be presented to
|
||||
other users.
|
||||
|
||||
* start/stop presence polling for my presence list
|
||||
It is likely that these messages could be implicitly inferred by other
|
||||
messages, though having explicit control is always useful.
|
||||
|
||||
* get my presence list
|
||||
[implicit poll start?]
|
||||
It is possible that the HS doesn't yet have current presence information when
|
||||
the client requests this. There should be a "don't know" type too.
|
||||
|
||||
* add/remove a user to my presence list
|
||||
|
||||
Server-Server
|
||||
-------------
|
||||
|
||||
Requests that Home Servers make to others
|
||||
|
||||
* request permission to add a user to presence list
|
||||
|
||||
* allow/deny a request to add to a presence list
|
||||
|
||||
* perform a combined presence state push and subscription request
|
||||
For each sending user ID, the message contains their new status.
|
||||
For each receiving user ID, the message should contain an indication on
|
||||
whether the sending server is also interested in receiving status from that
|
||||
user; either as an immediate update response now, or as a promise to send
|
||||
future updates.
|
||||
|
||||
Server to Client
|
||||
----------------
|
||||
|
||||
[[TODO(paul): There also needs to be some way for a user's HS to push status
|
||||
updates of the presence list to clients, but the general server-client event
|
||||
model currently lacks a space to do that.]]
|
||||
@@ -1,232 +0,0 @@
|
||||
========
|
||||
Profiles
|
||||
========
|
||||
|
||||
A description of Synapse user profile metadata support.
|
||||
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Internally within Synapse users are referred to by an opaque ID, which consists
|
||||
of some opaque localpart combined with the domain name of their home server.
|
||||
Obviously this does not yield a very nice user experience; users would like to
|
||||
see readable names for other users that are in some way meaningful to them.
|
||||
Additionally, users like to be able to publish "profile" details to inform other
|
||||
users of other information about them.
|
||||
|
||||
It is also conceivable that since we are attempting to provide a
|
||||
worldwide-applicable messaging system, that users may wish to present different
|
||||
subsets of information in their profile to different other people, from a
|
||||
privacy and permissions perspective.
|
||||
|
||||
A Profile consists of a display name, an (optional?) avatar picture, and a set
|
||||
of other metadata fields that the user may wish to publish (email address, phone
|
||||
numbers, website URLs, etc...). We put no requirements on the display name other
|
||||
than it being a valid Unicode string. Since it is likely that users will end up
|
||||
having multiple accounts (perhaps by necessity of being hosted in multiple
|
||||
places, perhaps by choice of wanting multiple distinct identifies), it would be
|
||||
useful that a metadata field type exists that can refer to another Synapse User
|
||||
ID, so that clients and HSes can make use of this information.
|
||||
|
||||
Metadata Fields
|
||||
---------------
|
||||
|
||||
[[TODO(paul): Likely this list is incomplete; more fields can be defined as we
|
||||
think of them. At the very least, any sort of supported ID for the 3rd Party ID
|
||||
servers should be accounted for here.]]
|
||||
|
||||
* Synapse Directory Server username(s)
|
||||
|
||||
* Email address
|
||||
|
||||
* Phone number - classify "home"/"work"/"mobile"/custom?
|
||||
|
||||
* Twitter/Facebook/Google+/... social networks
|
||||
|
||||
* Location - keep this deliberately vague to allow people to choose how
|
||||
granular it is
|
||||
|
||||
* "Bio" information - date of birth, etc...
|
||||
|
||||
* Synapse User ID of another account
|
||||
|
||||
* Web URL
|
||||
|
||||
* Freeform description text
|
||||
|
||||
|
||||
Visibility Permissions
|
||||
======================
|
||||
|
||||
A home server implementation could offer the ability to set permissions on
|
||||
limited visibility of those fields. When another user requests access to the
|
||||
target user's profile, their own identity should form part of that request. The
|
||||
HS implementation can then decide which fields to make available to the
|
||||
requestor.
|
||||
|
||||
A particular detail of implementation could allow the user to create one or more
|
||||
ACLs; where each list is granted permission to see a given set of non-public
|
||||
fields (compare to Google+ Circles) and contains a set of other people allowed
|
||||
to use it. By giving these ACLs strong identities within the HS, they can be
|
||||
referenced in communications with it, granting other users who encounter these
|
||||
the "ACL Token" to use the details in that ACL.
|
||||
|
||||
If we further allow an ACL Token to be present on Room join requests or stored
|
||||
by 3PID servers, then users of these ACLs gain the extra convenience of not
|
||||
having to manually curate people in the access list; anyone in the room or with
|
||||
knowledge of the 3rd Party ID is automatically granted access. Every HS and
|
||||
client implementation would have to be aware of the existence of these ACL
|
||||
Token, and include them in requests if present, but not every HS implementation
|
||||
needs to actually provide the full permissions model. This can be used as a
|
||||
distinguishing feature among competing implementations. However, servers MUST
|
||||
NOT serve profile information from a cache if there is a chance that its limited
|
||||
understanding could lead to information leakage.
|
||||
|
||||
|
||||
Client Concerns of Multiple Accounts
|
||||
====================================
|
||||
|
||||
Because a given person may want to have multiple Synapse User accounts, client
|
||||
implementations should allow the use of multiple accounts simultaneously
|
||||
(especially in the field of mobile phone clients, which generally don't support
|
||||
running distinct instances of the same application). Where features like address
|
||||
books, presence lists or rooms are presented, the client UI should remember to
|
||||
make distinct with user account is in use for each.
|
||||
|
||||
|
||||
Directory Servers
|
||||
=================
|
||||
|
||||
Directory Servers can provide a forward mapping from human-readable names to
|
||||
User IDs. These can provide a service similar to giving domain-namespaced names
|
||||
for Rooms; in this case they can provide a way for a user to reference their
|
||||
User ID in some external form (e.g. that can be printed on a business card).
|
||||
|
||||
The format for Synapse user name will consist of a localpart specific to the
|
||||
directory server, and the domain name of that directory server:
|
||||
|
||||
@localname:some.domain.name
|
||||
|
||||
The localname is separated from the domain name using a colon, so as to ensure
|
||||
the localname can still contain periods, as users may want this for similarity
|
||||
to email addresses or the like, which typically can contain them. The format is
|
||||
also visually quite distinct from email addresses, phone numbers, etc... so
|
||||
hopefully reasonably "self-describing" when written on e.g. a business card
|
||||
without surrounding context.
|
||||
|
||||
[[TODO(paul): we might have to think about this one - too close to email?
|
||||
Twitter? Also it suggests a format scheme for room names of
|
||||
#localname:domain.name, which I quite like]]
|
||||
|
||||
Directory server administrators should be able to make some kind of policy
|
||||
decision on how these are allocated. Servers within some "closed" domain (such
|
||||
as company-specific ones) may wish to verify the validity of a mapping using
|
||||
their own internal mechanisms; "public" naming servers can operate on a FCFS
|
||||
basis. There are overlapping concerns here with the idea of the 3rd party
|
||||
identity servers as well, though in this specific case we are creating a new
|
||||
namespace to allocate names into.
|
||||
|
||||
It would also be nice from a user experience perspective if the profile that a
|
||||
given name links to can also declare that name as part of its metadata.
|
||||
Furthermore as a security and consistency perspective it would be nice if each
|
||||
end (the directory server and the user's home server) check the validity of the
|
||||
mapping in some way. This needs investigation from a security perspective to
|
||||
ensure against spoofing.
|
||||
|
||||
One such model may be that the user starts by declaring their intent to use a
|
||||
given user name link to their home server, which then contacts the directory
|
||||
service. At some point later (maybe immediately for "public open FCFS servers",
|
||||
maybe after some kind of human intervention for verification) the DS decides to
|
||||
honour this link, and includes it in its served output. It should also tell the
|
||||
HS of this fact, so that the HS can present this as fact when requested for the
|
||||
profile information. For efficiency, it may further wish to provide the HS with
|
||||
a cryptographically-signed certificate as proof, so the HS serving the profile
|
||||
can provide that too when asked, avoiding requesting HSes from constantly having
|
||||
to contact the DS to verify this mapping. (Note: This is similar to the security
|
||||
model often applied in DNS to verify PTR <-> A bidirectional mappings).
|
||||
|
||||
|
||||
Identity Servers
|
||||
================
|
||||
|
||||
The identity servers should support the concept of pointing a 3PID being able to
|
||||
store an ACL Token as well as the main User ID. It is however, beyond scope to
|
||||
do any kind of verification that any third-party IDs that the profile is
|
||||
claiming match up to the 3PID mappings.
|
||||
|
||||
|
||||
User Interface and Expectations Concerns
|
||||
========================================
|
||||
|
||||
Given the weak "security" of some parts of this model as compared to what users
|
||||
might expect, some care should be taken on how it is presented to users,
|
||||
specifically in the naming or other wording of user interface components.
|
||||
|
||||
Most notably mere knowledge of an ACL Pointer is enough to read the information
|
||||
stored in it. It is possible that Home or Identity Servers could leak this
|
||||
information, allowing others to see it. This is a security-vs-convenience
|
||||
balancing choice on behalf of the user who would choose, or not, to make use of
|
||||
such a feature to publish their information.
|
||||
|
||||
Additionally, unless some form of strong end-to-end user-based encryption is
|
||||
used, a user of ACLs for information privacy has to trust other home servers not
|
||||
to lie about the identify of the user requesting access to the Profile.
|
||||
|
||||
|
||||
API Requirements
|
||||
================
|
||||
|
||||
The data model presented here puts the following requirements on the APIs:
|
||||
|
||||
Client-Server
|
||||
-------------
|
||||
|
||||
Requests that a client can make to its Home Server
|
||||
|
||||
* get/set my Display Name
|
||||
This should return/take a simple "text/plain" field
|
||||
|
||||
* get/set my Avatar URL
|
||||
The avatar image data itself is not stored by this API; we'll just store a
|
||||
URL to let the clients fetch it. Optionally HSes could integrate this with
|
||||
their generic content attacmhent storage service, allowing a user to set
|
||||
upload their profile Avatar and update the URL to point to it.
|
||||
|
||||
* get/add/remove my metadata fields
|
||||
Also we need to actually define types of metadata
|
||||
|
||||
* get another user's Display Name / Avatar / metadata fields
|
||||
|
||||
[[TODO(paul): At some later stage we should consider the API for:
|
||||
|
||||
* get/set ACL permissions on my metadata fields
|
||||
|
||||
* manage my ACL tokens
|
||||
]]
|
||||
|
||||
Server-Server
|
||||
-------------
|
||||
|
||||
Requests that Home Servers make to others
|
||||
|
||||
* get a user's Display Name / Avatar
|
||||
|
||||
* get a user's full profile - name/avatar + MD fields
|
||||
This request must allow for specifying the User ID of the requesting user,
|
||||
for permissions purposes. It also needs to take into account any ACL Tokens
|
||||
the requestor has.
|
||||
|
||||
* push a change of Display Name to observers (overlaps with the presence API)
|
||||
|
||||
Room Event PDU Types
|
||||
--------------------
|
||||
|
||||
Events that are pushed from Home Servers to other Home Servers or clients.
|
||||
|
||||
* user Display Name change
|
||||
|
||||
* user Avatar change
|
||||
[[TODO(paul): should the avatar image itself be stored in all the room
|
||||
histories? maybe this event should just be a hint to clients that they should
|
||||
re-fetch the avatar image]]
|
||||
@@ -1,64 +0,0 @@
|
||||
PUT /send/abc/ HTTP/1.1
|
||||
Host: ...
|
||||
Content-Length: ...
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"origin": "localhost:5000",
|
||||
"pdus": [
|
||||
{
|
||||
"content": {},
|
||||
"context": "tng",
|
||||
"depth": 12,
|
||||
"is_state": false,
|
||||
"origin": "localhost:5000",
|
||||
"pdu_id": 1404381396854,
|
||||
"pdu_type": "feedback",
|
||||
"prev_pdus": [
|
||||
[
|
||||
"1404381395883",
|
||||
"localhost:6000"
|
||||
]
|
||||
],
|
||||
"ts": 1404381427581
|
||||
}
|
||||
],
|
||||
"prev_ids": [
|
||||
"1404381396852"
|
||||
],
|
||||
"ts": 1404381427823
|
||||
}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
...
|
||||
|
||||
======================================
|
||||
|
||||
GET /pull/-1/ HTTP/1.1
|
||||
Host: ...
|
||||
Content-Length: 0
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: ...
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
origin: ...,
|
||||
prev_ids: ...,
|
||||
data: [
|
||||
{
|
||||
data_id: ...,
|
||||
prev_pdus: [...],
|
||||
depth: ...,
|
||||
ts: ...,
|
||||
context: ...,
|
||||
origin: ...,
|
||||
content: {
|
||||
...
|
||||
}
|
||||
},
|
||||
...,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
==================
|
||||
Room Join Workflow
|
||||
==================
|
||||
|
||||
An outline of the workflows required when a user joins a room.
|
||||
|
||||
Discovery
|
||||
=========
|
||||
|
||||
To join a room, a user has to discover the room by some mechanism in order to
|
||||
obtain the (opaque) Room ID and a candidate list of likely home servers that
|
||||
contain it.
|
||||
|
||||
Sending an Invitation
|
||||
---------------------
|
||||
|
||||
The most direct way a user discovers the existence of a room is from a
|
||||
invitation from some other user who is a member of that room.
|
||||
|
||||
The inviter's HS sets the membership status of the invitee to "invited" in the
|
||||
"m.members" state key by sending a state update PDU. The HS then broadcasts this
|
||||
PDU among the existing members in the usual way. An invitation message is also
|
||||
sent to the invited user, containing the Room ID and the PDU ID of this
|
||||
invitation state change and potentially a list of some other home servers to use
|
||||
to accept the invite. The user's client can then choose to display it in some
|
||||
way to alert the user.
|
||||
|
||||
[[TODO(paul): At present, no API has been designed or described to actually send
|
||||
that invite to the invited user. Likely it will be some facet of the larger
|
||||
user-user API required for presence, profile management, etc...]]
|
||||
|
||||
Directory Service
|
||||
-----------------
|
||||
|
||||
Alternatively, the user may discover the channel via a directory service; either
|
||||
by performing a name lookup, or some kind of browse or search acitivty. However
|
||||
this is performed, the end result is that the user's home server requests the
|
||||
Room ID and candidate list from the directory service.
|
||||
|
||||
[[TODO(paul): At present, no API has been designed or described for this
|
||||
directory service]]
|
||||
|
||||
|
||||
Joining
|
||||
=======
|
||||
|
||||
Once the ID and home servers are obtained, the user can then actually join the
|
||||
room.
|
||||
|
||||
Accepting an Invite
|
||||
-------------------
|
||||
|
||||
If a user has received and accepted an invitation to join a room, the invitee's
|
||||
home server can now send an invite acceptance message to a chosen candidate
|
||||
server from the list given in the invitation, citing also the PDU ID of the
|
||||
invitation as "proof" of their invite. (This is required as due to late message
|
||||
propagation it could be the case that the acceptance is received before the
|
||||
invite by some servers). If this message is allowed by the candidate server, it
|
||||
generates a new PDU that updates the invitee's membership status to "joined",
|
||||
referring back to the acceptance PDU, and broadcasts that as a state change in
|
||||
the usual way. The newly-invited user is now a full member of the room, and
|
||||
state propagation proceeds as usual.
|
||||
|
||||
Joining a Public Room
|
||||
---------------------
|
||||
|
||||
If a user has discovered the existence of a room they wish to join but does not
|
||||
have an active invitation, they can request to join it directly by sending a
|
||||
join message to a candidate server on the list provided by the directory
|
||||
service. As this list may be out of date, the HS should be prepared to retry
|
||||
other candidates if the chosen one is no longer aware of the room, because it
|
||||
has no users as members in it.
|
||||
|
||||
Once a candidate server that is aware of the room has been found, it can
|
||||
broadcast an update PDU to add the member into the "m.members" key setting their
|
||||
state directly to "joined" (i.e. bypassing the two-phase invite semantics),
|
||||
remembering to include the new user's HS in that list.
|
||||
|
||||
Knocking on a Semi-Public Room
|
||||
------------------------------
|
||||
|
||||
If a user requests to join a room but the join mode of the room is "knock", the
|
||||
join is not immediately allowed. Instead, if the user wishes to proceed, they
|
||||
can instead post a "knock" message, which informs other members of the room that
|
||||
the would-be joiner wishes to become a member and sets their membership value to
|
||||
"knocked". If any of them wish to accept this, they can then send an invitation
|
||||
in the usual way described above. Knowing that the user has already knocked and
|
||||
expressed an interest in joining, the invited user's home server should
|
||||
immediately accept that invitation on the user's behalf, and go on to join the
|
||||
room in the usual way.
|
||||
|
||||
[[NOTE(Erik): Though this may confuse users who expect 'X has joined' to
|
||||
actually be a user initiated action, i.e. they may expect that 'X' is actually
|
||||
looking at synapse right now?]]
|
||||
|
||||
[[NOTE(paul): Yes, a fair point maybe we should suggest HSes don't do that, and
|
||||
just offer an invite to the user as normal]]
|
||||
|
||||
Private and Non-Existent Rooms
|
||||
------------------------------
|
||||
|
||||
If a user requests to join a room but the room is either unknown by the home
|
||||
server receiving the request, or is known by the join mode is "invite" and the
|
||||
user has not been invited, the server must respond that the room does not exist.
|
||||
This is to prevent leaking information about the existence and identity of
|
||||
private rooms.
|
||||
|
||||
|
||||
Outstanding Questions
|
||||
=====================
|
||||
|
||||
* Do invitations or knocks time out and expire at some point? If so when? Time
|
||||
is hard in distributed systems.
|
||||
@@ -1,274 +0,0 @@
|
||||
===========
|
||||
Rooms Model
|
||||
===========
|
||||
|
||||
A description of the general data model used to implement Rooms, and the
|
||||
user-level visible effects and implications.
|
||||
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
"Rooms" in Synapse are shared messaging channels over which all the participant
|
||||
users can exchange messages. Rooms have an opaque persistent identify, a
|
||||
globally-replicated set of state (consisting principly of a membership set of
|
||||
users, and other management and miscellaneous metadata), and a message history.
|
||||
|
||||
|
||||
Room Identity and Naming
|
||||
========================
|
||||
|
||||
Rooms can be arbitrarily created by any user on any home server; at which point
|
||||
the home server will sign the message that creates the channel, and the
|
||||
fingerprint of this signature becomes the strong persistent identify of the
|
||||
room. This now identifies the room to any home server in the network regardless
|
||||
of its original origin. This allows the identify of the room to outlive any
|
||||
particular server. Subject to appropriate permissions [to be discussed later],
|
||||
any current member of a room can invite others to join it, can post messages
|
||||
that become part of its history, and can change the persistent state of the room
|
||||
(including its current set of permissions).
|
||||
|
||||
Home servers can provide a directory service, allowing a lookup from a
|
||||
convenient human-readable form of room label to a room ID. This mapping is
|
||||
scoped to the particular home server domain and so simply represents that server
|
||||
administrator's opinion of what room should take that label; it does not have to
|
||||
be globally replicated and does not form part of the stored state of that room.
|
||||
|
||||
This room name takes the form
|
||||
|
||||
#localname:some.domain.name
|
||||
|
||||
for similarity and consistency with user names on directories.
|
||||
|
||||
To join a room (and therefore to be allowed to inspect past history, post new
|
||||
messages to it, and read its state), a user must become aware of the room's
|
||||
fingerprint ID. There are two mechanisms to allow this:
|
||||
|
||||
* An invite message from someone else in the room
|
||||
|
||||
* A referral from a room directory service
|
||||
|
||||
As room IDs are opaque and ephemeral, they can serve as a mechanism to create
|
||||
"ad-hoc" rooms deliberately unnamed, for small group-chats or even private
|
||||
one-to-one message exchange.
|
||||
|
||||
|
||||
Stored State and Permissions
|
||||
============================
|
||||
|
||||
Every room has a globally-replicated set of stored state. This state is a set of
|
||||
key/value or key/subkey/value pairs. The value of every (sub)key is a
|
||||
JSON-representable object. The main key of a piece of stored state establishes
|
||||
its meaning; some keys store sub-keys to allow a sub-structure within them [more
|
||||
detail below]. Some keys have special meaning to Synapse, as they relate to
|
||||
management details of the room itself, storing such details as user membership,
|
||||
and permissions of users to alter the state of the room itself. Other keys may
|
||||
store information to present to users, which the system does not directly rely
|
||||
on. The key space itself is namespaced, allowing 3rd party extensions, subject
|
||||
to suitable permission.
|
||||
|
||||
Permission management is based on the concept of "power-levels". Every user
|
||||
within a room has an integer assigned, being their "power-level" within that
|
||||
room. Along with its actual data value, each key (or subkey) also stores the
|
||||
minimum power-level a user must have in order to write to that key, the
|
||||
power-level of the last user who actually did write to it, and the PDU ID of
|
||||
that state change.
|
||||
|
||||
To be accepted as valid, a change must NOT:
|
||||
|
||||
* Be made by a user having a power-level lower than required to write to the
|
||||
state key
|
||||
|
||||
* Alter the required power-level for that state key to a value higher than the
|
||||
user has
|
||||
|
||||
* Increase that user's own power-level
|
||||
|
||||
* Grant any other user a power-level higher than the level of the user making
|
||||
the change
|
||||
|
||||
[[TODO(paul): consider if relaxations should be allowed; e.g. is the current
|
||||
outright-winner allowed to raise their own level, to allow for "inflation"?]]
|
||||
|
||||
|
||||
Room State Keys
|
||||
===============
|
||||
|
||||
[[TODO(paul): if this list gets too big it might become necessary to move it
|
||||
into its own doc]]
|
||||
|
||||
The following keys have special semantics or meaning to Synapse itself:
|
||||
|
||||
m.member (has subkeys)
|
||||
Stores a sub-key for every Synapse User ID which is currently a member of
|
||||
this room. Its value gives the membership type ("knocked", "invited",
|
||||
"joined").
|
||||
|
||||
m.power_levels
|
||||
Stores a mapping from Synapse User IDs to their power-level in the room. If
|
||||
they are not present in this mapping, the default applies.
|
||||
|
||||
The reason to store this as a single value rather than a value with subkeys
|
||||
is that updates to it are atomic; allowing a number of colliding-edit
|
||||
problems to be avoided.
|
||||
|
||||
m.default_level
|
||||
Gives the default power-level for members of the room that do not have one
|
||||
specified in their membership key.
|
||||
|
||||
m.invite_level
|
||||
If set, gives the minimum power-level required for members to invite others
|
||||
to join, or to accept knock requests from non-members requesting access. If
|
||||
absent, then invites are not allowed. An invitation involves setting their
|
||||
membership type to "invited", in addition to sending the invite message.
|
||||
|
||||
m.join_rules
|
||||
Encodes the rules on how non-members can join the room. Has the following
|
||||
possibilities:
|
||||
"public" - a non-member can join the room directly
|
||||
"knock" - a non-member cannot join the room, but can post a single "knock"
|
||||
message requesting access, which existing members may approve or deny
|
||||
"invite" - non-members cannot join the room without an invite from an
|
||||
existing member
|
||||
"private" - nobody who is not in the 'may_join' list or already a member
|
||||
may join by any mechanism
|
||||
|
||||
In any of the first three modes, existing members with sufficient permission
|
||||
can send invites to non-members if allowed by the "m.invite_level" key. A
|
||||
"private" room is not allowed to have the "m.invite_level" set.
|
||||
|
||||
A client may use the value of this key to hint at the user interface
|
||||
expectations to provide; in particular, a private chat with one other use
|
||||
might warrant specific handling in the client.
|
||||
|
||||
m.may_join
|
||||
A list of User IDs that are always allowed to join the room, regardless of any
|
||||
of the prevailing join rules and invite levels. These apply even to private
|
||||
rooms. These are stored in a single list with normal update-powerlevel
|
||||
permissions applied; users cannot arbitrarily remove themselves from the list.
|
||||
|
||||
m.add_state_level
|
||||
The power-level required for a user to be able to add new state keys.
|
||||
|
||||
m.public_history
|
||||
If set and true, anyone can request the history of the room, without needing
|
||||
to be a member of the room.
|
||||
|
||||
m.archive_servers
|
||||
For "public" rooms with public history, gives a list of home servers that
|
||||
should be included in message distribution to the room, even if no users on
|
||||
that server are present. These ensure that a public room can still persist
|
||||
even if no users are currently members of it. This list should be consulted by
|
||||
the dirctory servers as the candidate list they respond with.
|
||||
|
||||
The following keys are provided by Synapse for user benefit, but their value is
|
||||
not otherwise used by Synapse.
|
||||
|
||||
m.name
|
||||
Stores a short human-readable name for the room, such that clients can display
|
||||
to a user to assist in identifying which room is which.
|
||||
|
||||
This name specifically is not the strong ID used by the message transport
|
||||
system to refer to the room, because it may be changed from time to time.
|
||||
|
||||
m.topic
|
||||
Stores the current human-readable topic
|
||||
|
||||
|
||||
Room Creation Templates
|
||||
=======================
|
||||
|
||||
A client (or maybe home server?) could offer a few templates for the creation of
|
||||
new rooms. For example, for a simple private one-to-one chat the channel could
|
||||
assign the creator a power-level of 1, requiring a level of 1 to invite, and
|
||||
needing an invite before members can join. An invite is then sent to the other
|
||||
party, and if accepted and the other user joins, the creator's power-level can
|
||||
now be reduced to 0. This now leaves a room with two participants in it being
|
||||
unable to add more.
|
||||
|
||||
|
||||
Rooms that Continue History
|
||||
===========================
|
||||
|
||||
An option that could be considered for room creation, is that when a new room is
|
||||
created the creator could specify a PDU ID into an existing room, as the history
|
||||
continuation point. This would be stored as an extra piece of meta-data on the
|
||||
initial PDU of the room's creation. (It does not appear in the normal previous
|
||||
PDU linkage).
|
||||
|
||||
This would allow users in rooms to "fork" a room, if it is considered that the
|
||||
conversations in the room no longer fit its original purpose, and wish to
|
||||
diverge. Existing permissions on the original room would continue to apply of
|
||||
course, for viewing that history. If both rooms are considered "public" we might
|
||||
also want to define a message to post into the original room to represent this
|
||||
fork point, and give a reference to the new room.
|
||||
|
||||
|
||||
User Direct Message Rooms
|
||||
=========================
|
||||
|
||||
There is no need to build a mechanism for directly sending messages between
|
||||
users, because a room can handle this ability. To allow direct user-to-user chat
|
||||
messaging we simply need to be able to create rooms with specific set of
|
||||
permissions to allow this direct messaging.
|
||||
|
||||
Between any given pair of user IDs that wish to exchange private messages, there
|
||||
will exist a single shared Room, created lazily by either side. These rooms will
|
||||
need a certain amount of special handling in both home servers and display on
|
||||
clients, but as much as possible should be treated by the lower layers of code
|
||||
the same as other rooms.
|
||||
|
||||
Specially, a client would likely offer a special menu choice associated with
|
||||
another user (in room member lists, presence list, etc..) as "direct chat". That
|
||||
would perform all the necessary steps to create the private chat room. Receiving
|
||||
clients should display these in a special way too as the room name is not
|
||||
important; instead it should distinguish them on the Display Name of the other
|
||||
party.
|
||||
|
||||
Home Servers will need a client-API option to request setting up a new user-user
|
||||
chat room, which will then need special handling within the server. It will
|
||||
create a new room with the following
|
||||
|
||||
m.member: the proposing user
|
||||
m.join_rules: "private"
|
||||
m.may_join: both users
|
||||
m.power_levels: empty
|
||||
m.default_level: 0
|
||||
m.add_state_level: 0
|
||||
m.public_history: False
|
||||
|
||||
Having created the room, it can send an invite message to the other user in the
|
||||
normal way - the room permissions state that no users can be set to the invited
|
||||
state, but because they're in the may_join list then they'd be allowed to join
|
||||
anyway.
|
||||
|
||||
In this arrangement there is now a room with both users may join but neither has
|
||||
the power to invite any others. Both users now have the confidence that (at
|
||||
least within the messaging system itself) their messages remain private and
|
||||
cannot later be provably leaked to a third party. They can freely set the topic
|
||||
or name if they choose and add or edit any other state of the room. The update
|
||||
powerlevel of each of these fixed properties should be 1, to lock out the users
|
||||
from being able to alter them.
|
||||
|
||||
|
||||
Anti-Glare
|
||||
==========
|
||||
|
||||
There exists the possibility of a race condition if two users who have no chat
|
||||
history with each other simultaneously create a room and invite the other to it.
|
||||
This is called a "glare" situation. There are two possible ideas for how to
|
||||
resolve this:
|
||||
|
||||
* Each Home Server should persist the mapping of (user ID pair) to room ID, so
|
||||
that duplicate requests can be suppressed. On receipt of a room creation
|
||||
request that the HS thinks there already exists a room for, the invitation to
|
||||
join can be rejected if:
|
||||
a) the HS believes the sending user is already a member of the room (and
|
||||
maybe their HS has forgotten this fact), or
|
||||
b) the proposed room has a lexicographically-higher ID than the existing
|
||||
room (to resolve true race condition conflicts)
|
||||
|
||||
* The room ID for a private 1:1 chat has a special form, determined by
|
||||
concatenting the User IDs of both members in a deterministic order, such that
|
||||
it doesn't matter which side creates it first; the HSes can just ignore
|
||||
(or merge?) received PDUs that create the room twice.
|
||||
@@ -1,86 +0,0 @@
|
||||
===========
|
||||
Terminology
|
||||
===========
|
||||
|
||||
A list of definitions of specific terminology used among these documents.
|
||||
These terms were originally taken from the server-server documentation, and may
|
||||
not currently match the exact meanings used in other places; though as a
|
||||
medium-term goal we should encourage the unification of this terminology.
|
||||
|
||||
|
||||
Terms
|
||||
=====
|
||||
|
||||
Backfilling:
|
||||
The process of synchronising historic state from one home server to another,
|
||||
to backfill the event storage so that scrollback can be presented to the
|
||||
client(s). (Formerly, and confusingly, called 'pagination')
|
||||
|
||||
Context:
|
||||
A single human-level entity of interest (currently, a chat room)
|
||||
|
||||
EDU (Ephemeral Data Unit):
|
||||
A message that relates directly to a given pair of home servers that are
|
||||
exchanging it. EDUs are short-lived messages that related only to one single
|
||||
pair of servers; they are not persisted for a long time and are not forwarded
|
||||
on to other servers. Because of this, they have no internal ID nor previous
|
||||
EDUs reference chain.
|
||||
|
||||
Event:
|
||||
A record of activity that records a single thing that happened on to a context
|
||||
(currently, a chat room). These are the "chat messages" that Synapse makes
|
||||
available.
|
||||
[[NOTE(paul): The current server-server implementation calls these simply
|
||||
"messages" but the term is too ambiguous here; I've called them Events]]
|
||||
|
||||
PDU (Persistent Data Unit):
|
||||
A message that relates to a single context, irrespective of the server that
|
||||
is communicating it. PDUs either encode a single Event, or a single State
|
||||
change. A PDU is referred to by its PDU ID; the pair of its origin server
|
||||
and local reference from that server.
|
||||
|
||||
PDU ID:
|
||||
The pair of PDU Origin and PDU Reference, that together globally uniquely
|
||||
refers to a specific PDU.
|
||||
|
||||
PDU Origin:
|
||||
The name of the origin server that generated a given PDU. This may not be the
|
||||
server from which it has been received, due to the way they are copied around
|
||||
from server to server. The origin always records the original server that
|
||||
created it.
|
||||
|
||||
PDU Reference:
|
||||
A local ID used to refer to a specific PDU from a given origin server. These
|
||||
references are opaque at the protocol level, but may optionally have some
|
||||
structured meaning within a given origin server or implementation.
|
||||
|
||||
Presence:
|
||||
The concept of whether a user is currently online, how available they declare
|
||||
they are, and so on. See also: doc/model/presence
|
||||
|
||||
Profile:
|
||||
A set of metadata about a user, such as a display name, provided for the
|
||||
benefit of other users. See also: doc/model/profiles
|
||||
|
||||
Room ID:
|
||||
An opaque string (of as-yet undecided format) that identifies a particular
|
||||
room and used in PDUs referring to it.
|
||||
|
||||
Room Alias:
|
||||
A human-readable string of the form #name:some.domain that users can use as a
|
||||
pointer to identify a room; a Directory Server will map this to its Room ID
|
||||
|
||||
State:
|
||||
A set of metadata maintained about a Context, which is replicated among the
|
||||
servers in addition to the history of Events.
|
||||
|
||||
User ID:
|
||||
A string of the form @localpart:domain.name that identifies a user for
|
||||
wire-protocol purposes. The localpart is meaningless outside of a particular
|
||||
home server. This takes a human-readable form that end-users can use directly
|
||||
if they so wish, avoiding the 3PIDs.
|
||||
|
||||
Transaction:
|
||||
A message which relates to the communication between a given pair of servers.
|
||||
A transaction contains possibly-empty lists of PDUs and EDUs.
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
======================
|
||||
Third Party Identities
|
||||
======================
|
||||
|
||||
A description of how email addresses, mobile phone numbers and other third
|
||||
party identifiers can be used to authenticate and discover users in Matrix.
|
||||
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
New users need to authenticate their account. An email or SMS text message can
|
||||
be a convenient form of authentication. Users already have email addresses
|
||||
and phone numbers for contacts in their address book. They want to communicate
|
||||
with those contacts in Matrix without manually exchanging a Matrix User ID with
|
||||
them.
|
||||
|
||||
Third Party IDs
|
||||
---------------
|
||||
|
||||
[[TODO(markjh): Describe the format of a 3PID]]
|
||||
|
||||
|
||||
Third Party ID Associations
|
||||
---------------------------
|
||||
|
||||
An Associaton is a binding between a Matrix User ID and a Third Party ID (3PID).
|
||||
Each 3PID can be associated with one Matrix User ID at a time.
|
||||
|
||||
[[TODO(markjh): JSON format of the association.]]
|
||||
|
||||
Verification
|
||||
------------
|
||||
|
||||
An Assocation must be verified by a trusted Verification Server. Email
|
||||
addresses and phone numbers can be verified by sending a token to the address
|
||||
which a client can supply to the verifier to confirm ownership.
|
||||
|
||||
An email Verification Server may be capable of verifying all email 3PIDs or may
|
||||
be restricted to verifying addresses for a particular domain. A phone number
|
||||
Verification Server may be capable of verifying all phone numbers or may be
|
||||
restricted to verifying numbers for a given country or phone prefix.
|
||||
|
||||
Verification Servers fulfil a similar role to Certificate Authorities in PKI so
|
||||
a similar level of vetting should be required before clients trust their
|
||||
signatures.
|
||||
|
||||
A Verification Server may wish to check for existing Associations for a 3PID
|
||||
before creating a new Association.
|
||||
|
||||
Discovery
|
||||
---------
|
||||
|
||||
Users can discover Associations using a trusted Identity Server. Each
|
||||
Association will be signed by the Identity Server. An Identity Server may store
|
||||
the entire space of Associations or may delegate to other Identity Servers when
|
||||
looking up Associations.
|
||||
|
||||
Each Association returned from an Identity Server must be signed by a
|
||||
Verification Server. Clients should check these signatures.
|
||||
|
||||
Identity Servers fulfil a similar role to DNS servers.
|
||||
|
||||
Privacy
|
||||
-------
|
||||
|
||||
A User may publish the association between their phone number and Matrix User ID
|
||||
on the Identity Server without publishing the number in their Profile hosted on
|
||||
their Home Server.
|
||||
|
||||
Identity Servers should refrain from publishing reverse mappings and should
|
||||
take steps, such as rate limiting, to prevent attackers enumerating the space of
|
||||
mappings.
|
||||
|
||||
Federation
|
||||
==========
|
||||
|
||||
Delegation
|
||||
----------
|
||||
|
||||
Verification Servers could delegate signing to another server by issuing
|
||||
certificate to that server allowing it to verify and sign a subset of 3PID on
|
||||
its behalf. It would be necessary to provide a language for describing which
|
||||
subset of 3PIDs that server had authority to validate. Alternatively it could
|
||||
delegate the verification step to another server but sign the resulting
|
||||
association itself.
|
||||
|
||||
The 3PID space will have a heirachical structure like DNS so Identity Servers
|
||||
can delegate lookups to other servers. An Identity Server should be prepared
|
||||
to host or delegate any valid association within the subset of the 3PIDs it is
|
||||
resonsible for.
|
||||
|
||||
Multiple Root Verification Servers
|
||||
----------------------------------
|
||||
|
||||
There can be multiple root Verification Servers and an Association could be
|
||||
signed by multiple servers if different clients trust different subsets of
|
||||
the verification servers.
|
||||
|
||||
Multiple Root Identity Servers
|
||||
------------------------------
|
||||
|
||||
There can be be multiple root Identity Servers. Clients will add each
|
||||
Association to all root Identity Servers.
|
||||
|
||||
[[TODO(markjh): Describe how clients find the list of root Identity Servers]]
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
Transaction
|
||||
===========
|
||||
|
||||
Required keys:
|
||||
|
||||
============ =================== ===============================================
|
||||
Key Type Description
|
||||
============ =================== ===============================================
|
||||
origin String DNS name of homeserver making this transaction.
|
||||
ts Integer Timestamp in milliseconds on originating
|
||||
homeserver when this transaction started.
|
||||
previous_ids List of Strings List of transactions that were sent immediately
|
||||
prior to this transaction.
|
||||
pdus List of Objects List of updates contained in this transaction.
|
||||
============ =================== ===============================================
|
||||
|
||||
|
||||
PDU
|
||||
===
|
||||
|
||||
Required keys:
|
||||
|
||||
============ ================== ================================================
|
||||
Key Type Description
|
||||
============ ================== ================================================
|
||||
context String Event context identifier
|
||||
origin String DNS name of homeserver that created this PDU.
|
||||
pdu_id String Unique identifier for PDU within the context for
|
||||
the originating homeserver.
|
||||
ts Integer Timestamp in milliseconds on originating
|
||||
homeserver when this PDU was created.
|
||||
pdu_type String PDU event type.
|
||||
prev_pdus List of Pairs The originating homeserver and PDU ids of the
|
||||
of Strings most recent PDUs the homeserver was aware of for
|
||||
this context when it made this PDU.
|
||||
depth Integer The maximum depth of the previous PDUs plus one.
|
||||
============ ================== ================================================
|
||||
|
||||
Keys for state updates:
|
||||
|
||||
================== ============ ================================================
|
||||
Key Type Description
|
||||
================== ============ ================================================
|
||||
is_state Boolean True if this PDU is updating state.
|
||||
state_key String Optional key identifying the updated state within
|
||||
the context.
|
||||
power_level Integer The asserted power level of the user performing
|
||||
the update.
|
||||
min_update Integer The required power level needed to replace this
|
||||
update.
|
||||
prev_state_id String The homeserver of the update this replaces
|
||||
prev_state_origin String The PDU id of the update this replaces.
|
||||
user String The user updating the state.
|
||||
================== ============ ================================================
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
Overview
|
||||
========
|
||||
|
||||
Scope
|
||||
-----
|
||||
|
||||
This document considers threats specific to the server to server federation
|
||||
synapse protocol.
|
||||
|
||||
|
||||
Attacker
|
||||
--------
|
||||
|
||||
It is assumed that the attacker can see and manipulate all network traffic
|
||||
between any of the servers and may be in control of one or more homeservers
|
||||
participating in the federation protocol.
|
||||
|
||||
Threat Model
|
||||
============
|
||||
|
||||
Denial of Service
|
||||
-----------------
|
||||
|
||||
The attacker could attempt to prevent delivery of messages to or from the
|
||||
victim in order to:
|
||||
|
||||
* Disrupt service or marketing campaign of a commercial competitor.
|
||||
* Censor a discussion or censor a participant in a discussion.
|
||||
* Perform general vandalism.
|
||||
|
||||
Threat: Resource Exhaustion
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could cause the victims server to exhaust a particular resource
|
||||
(e.g. open TCP connections, CPU, memory, disk storage)
|
||||
|
||||
Threat: Unrecoverable Consistency Violations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could send messages which created an unrecoverable "split-brain"
|
||||
state in the cluster such that the victim's servers could no longer dervive a
|
||||
consistent view of the chatroom state.
|
||||
|
||||
Threat: Bad History
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could convince the victim to accept invalid messages which the
|
||||
victim would then include in their view of the chatroom history. Other servers
|
||||
in the chatroom would reject the invalid messages and potentially reject the
|
||||
victims messages as well since they depended on the invalid messages.
|
||||
|
||||
Threat: Block Network Traffic
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could try to firewall traffic between the victim's server and some
|
||||
or all of the other servers in the chatroom.
|
||||
|
||||
Threat: High Volume of Messages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could send large volumes of messages to a chatroom with the victim
|
||||
making the chatroom unusable.
|
||||
|
||||
Threat: Banning users without necessary authorisation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could attempt to ban a user from a chatroom with the necessary
|
||||
authorisation.
|
||||
|
||||
Spoofing
|
||||
--------
|
||||
|
||||
An attacker could try to send a message claiming to be from the victim without
|
||||
the victim having sent the message in order to:
|
||||
|
||||
* Impersonate the victim while performing illict activity.
|
||||
* Obtain privileges of the victim.
|
||||
|
||||
Threat: Altering Message Contents
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could try to alter the contents of an existing message from the
|
||||
victim.
|
||||
|
||||
Threat: Fake Message "origin" Field
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could try to send a new message purporting to be from the victim
|
||||
with a phony "origin" field.
|
||||
|
||||
Spamming
|
||||
--------
|
||||
|
||||
The attacker could try to send a high volume of solicicted or unsolicted
|
||||
messages to the victim in order to:
|
||||
|
||||
* Find victims for scams.
|
||||
* Market unwanted products.
|
||||
|
||||
Threat: Unsoliticted Messages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could try to send messages to victims who do not wish to receive
|
||||
them.
|
||||
|
||||
Threat: Abusive Messages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could send abusive or threatening messages to the victim
|
||||
|
||||
Spying
|
||||
------
|
||||
|
||||
The attacker could try to access message contents or metadata for messages sent
|
||||
by the victim or to the victim that were not intended to reach the attacker in
|
||||
order to:
|
||||
|
||||
* Gain sensitive personal or commercial information.
|
||||
* Impersonate the victim using credentials contained in the messages.
|
||||
(e.g. password reset messages)
|
||||
* Discover who the victim was talking to and when.
|
||||
|
||||
Threat: Disclosure during Transmission
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could try to expose the message contents or metadata during
|
||||
transmission between the servers.
|
||||
|
||||
Threat: Disclosure to Servers Outside Chatroom
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could try to convince servers within a chatroom to send messages to
|
||||
a server it controls that was not authorised to be within the chatroom.
|
||||
|
||||
Threat: Disclosure to Servers Within Chatroom
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An attacker could take control of a server within a chatroom to expose message
|
||||
contents or metadata for messages in that room.
|
||||
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
===========================
|
||||
Matrix Server-to-Server API
|
||||
===========================
|
||||
|
||||
A description of the protocol used to communicate between Matrix home servers;
|
||||
also known as Federation.
|
||||
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
The server-server API is a mechanism by which two home servers can exchange
|
||||
Matrix event messages, both as a real-time push of current events, and as a
|
||||
historic fetching mechanism to synchronise past history for clients to view. It
|
||||
uses HTTP connections between each pair of servers involved as the underlying
|
||||
transport. Messages are exchanged between servers in real-time by active pushing
|
||||
from each server's HTTP client into the server of the other. Queries to fetch
|
||||
historic data for the purpose of back-filling scrollback buffers and the like
|
||||
can also be performed.
|
||||
|
||||
|
||||
{ Matrix clients } { Matrix clients }
|
||||
^ | ^ |
|
||||
| events | | events |
|
||||
| V | V
|
||||
+------------------+ +------------------+
|
||||
| |---------( HTTP )---------->| |
|
||||
| Home Server | | Home Server |
|
||||
| |<--------( HTTP )-----------| |
|
||||
+------------------+ +------------------+
|
||||
|
||||
There are three main kinds of communication that occur between home servers:
|
||||
|
||||
* Queries
|
||||
These are single request/response interactions between a given pair of
|
||||
servers, initiated by one side sending an HTTP request to obtain some
|
||||
information, and responded by the other. They are not persisted and contain
|
||||
no long-term significant history. They simply request a snapshot state at the
|
||||
instant the query is made.
|
||||
|
||||
* EDUs - Ephemeral Data Units
|
||||
These are notifications of events that are pushed from one home server to
|
||||
another. They are not persisted and contain no long-term significant history,
|
||||
nor does the receiving home server have to reply to them.
|
||||
|
||||
* PDUs - Persisted Data Units
|
||||
These are notifications of events that are broadcast from one home server to
|
||||
any others that are interested in the same "context" (namely, a Room ID).
|
||||
They are persisted to long-term storage and form the record of history for
|
||||
that context.
|
||||
|
||||
Where Queries are presented directly across the HTTP connection as GET requests
|
||||
to specific URLs, EDUs and PDUs are further wrapped in an envelope called a
|
||||
Transaction, which is transferred from the origin to the destination home server
|
||||
using a PUT request.
|
||||
|
||||
|
||||
Transactions and EDUs/PDUs
|
||||
==========================
|
||||
|
||||
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
||||
of Transaction messages, which are encoded as JSON objects with a dict as the
|
||||
top-level element, passed over an HTTP PUT request. A Transaction is meaningful
|
||||
only to the pair of home servers that exchanged it; they are not globally-
|
||||
meaningful.
|
||||
|
||||
Each transaction has an opaque ID and timestamp (UNIX epoch time in
|
||||
milliseconds) generated by its origin server, an origin and destination server
|
||||
name, a list of "previous IDs", and a list of PDUs - the actual message payload
|
||||
that the Transaction carries.
|
||||
|
||||
{"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
||||
"ts":1404835423000,
|
||||
"origin":"red",
|
||||
"destination":"blue",
|
||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
||||
"pdus":[...],
|
||||
"edus":[...]}
|
||||
|
||||
The "previous IDs" field will contain a list of previous transaction IDs that
|
||||
the origin server has sent to this destination. Its purpose is to act as a
|
||||
sequence checking mechanism - the destination server can check whether it has
|
||||
successfully received that Transaction, or ask for a retransmission if not.
|
||||
|
||||
The "pdus" field of a transaction is a list, containing zero or more PDUs.[*]
|
||||
Each PDU is itself a dict containing a number of keys, the exact details of
|
||||
which will vary depending on the type of PDU. Similarly, the "edus" field is
|
||||
another list containing the EDUs. This key may be entirely absent if there are
|
||||
no EDUs to transfer.
|
||||
|
||||
(* Normally the PDU list will be non-empty, but the server should cope with
|
||||
receiving an "empty" transaction, as this is useful for informing peers of other
|
||||
transaction IDs they should be aware of. This effectively acts as a push
|
||||
mechanism to encourage peers to continue to replicate content.)
|
||||
|
||||
All PDUs have an ID, a context, a declaration of their type, a list of other PDU
|
||||
IDs that have been seen recently on that context (regardless of which origin
|
||||
sent them), and a nested content field containing the actual event content.
|
||||
|
||||
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
|
||||
[origin,ref] pair like the prev_pdus are]]
|
||||
|
||||
{"pdu_id":"a4ecee13e2accdadf56c1025af232176",
|
||||
"context":"#example.green",
|
||||
"origin":"green",
|
||||
"ts":1404838188000,
|
||||
"pdu_type":"m.text",
|
||||
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
|
||||
"content":...
|
||||
"is_state":false}
|
||||
|
||||
In contrast to the transaction layer, it is important to note that the prev_pdus
|
||||
field of a PDU refers to PDUs that any origin server has sent, rather than
|
||||
previous IDs that this origin has sent. This list may refer to other PDUs sent
|
||||
by the same origin as the current one, or other origins.
|
||||
|
||||
Because of the distributed nature of participants in a Matrix conversation, it
|
||||
is impossible to establish a globally-consistent total ordering on the events.
|
||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
||||
has received, a partial ordering can be constructed allowing causallity
|
||||
relationships to be preserved. A client can then display these messages to the
|
||||
end-user in some order consistent with their content and ensure that no message
|
||||
that is semantically in reply of an earlier one is ever displayed before it.
|
||||
|
||||
PDUs fall into two main categories: those that deliver Events, and those that
|
||||
synchronise State. For PDUs that relate to State synchronisation, additional
|
||||
keys exist to support this:
|
||||
|
||||
{...,
|
||||
"is_state":true,
|
||||
"state_key":TODO
|
||||
"power_level":TODO
|
||||
"prev_state_id":TODO
|
||||
"prev_state_origin":TODO}
|
||||
|
||||
[[TODO(paul): At this point we should probably have a long description of how
|
||||
State management works, with descriptions of clobbering rules, power levels, etc
|
||||
etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
|
||||
so on. This part needs refining. And writing in its own document as the details
|
||||
relate to the server/system as a whole, not specifically to server-server
|
||||
federation.]]
|
||||
|
||||
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
||||
"previous" IDs. The only mandatory fields for these are the type, origin and
|
||||
destination home server names, and the actual nested content.
|
||||
|
||||
{"edu_type":"m.presence",
|
||||
"origin":"blue",
|
||||
"destination":"orange",
|
||||
"content":...}
|
||||
|
||||
|
||||
Protocol URLs
|
||||
=============
|
||||
|
||||
All these URLs are namespaced within a prefix of
|
||||
|
||||
/matrix/federation/v1/...
|
||||
|
||||
For active pushing of messages representing live activity "as it happens":
|
||||
|
||||
PUT .../send/:transaction_id/
|
||||
Body: JSON encoding of a single Transaction
|
||||
|
||||
Response: [[TODO(paul): I don't actually understand what
|
||||
ReplicationLayer.on_transaction() is doing here, so I'm not sure what the
|
||||
response ought to be]]
|
||||
|
||||
The transaction_id path argument will override any ID given in the JSON body.
|
||||
The destination name will be set to that of the receiving server itself. Each
|
||||
embedded PDU in the transaction body will be processed.
|
||||
|
||||
|
||||
To fetch a particular PDU:
|
||||
|
||||
GET .../pdu/:origin/:pdu_id/
|
||||
|
||||
Response: JSON encoding of a single Transaction containing one PDU
|
||||
|
||||
Retrieves a given PDU from the server. The response will contain a single new
|
||||
Transaction, inside which will be the requested PDU.
|
||||
|
||||
|
||||
To fetch all the state of a given context:
|
||||
|
||||
GET .../state/:context/
|
||||
|
||||
Response: JSON encoding of a single Transaction containing multiple PDUs
|
||||
|
||||
Retrieves a snapshot of the entire current state of the given context. The
|
||||
response will contain a single Transaction, inside which will be a list of
|
||||
PDUs that encode the state.
|
||||
|
||||
|
||||
To backfill events on a given context:
|
||||
|
||||
GET .../backfill/:context/
|
||||
Query args: v, limit
|
||||
|
||||
Response: JSON encoding of a single Transaction containing multiple PDUs
|
||||
|
||||
Retrieves a sliding-window history of previous PDUs that occurred on the
|
||||
given context. Starting from the PDU ID(s) given in the "v" argument, the
|
||||
PDUs that preceeded it are retrieved, up to a total number given by the
|
||||
"limit" argument. These are then returned in a new Transaction containing all
|
||||
off the PDUs.
|
||||
|
||||
|
||||
To stream events all the events:
|
||||
|
||||
GET .../pull/
|
||||
Query args: origin, v
|
||||
|
||||
Response: JSON encoding of a single Transaction consisting of multiple PDUs
|
||||
|
||||
Retrieves all of the transactions later than any version given by the "v"
|
||||
arguments. [[TODO(paul): I'm not sure what the "origin" argument does because
|
||||
I think at some point in the code it's got swapped around.]]
|
||||
|
||||
|
||||
To make a query:
|
||||
|
||||
GET .../query/:query_type
|
||||
Query args: as specified by the individual query types
|
||||
|
||||
Response: JSON encoding of a response object
|
||||
|
||||
Performs a single query request on the receiving home server. The Query Type
|
||||
part of the path specifies the kind of query being made, and its query
|
||||
arguments have a meaning specific to that kind of query. The response is a
|
||||
JSON-encoded object whose meaning also depends on the kind of query.
|
||||
@@ -1,11 +0,0 @@
|
||||
Versioning is, like, hard for backfilling backwards because of the number of Home Servers involved.
|
||||
|
||||
The way we solve this is by doing versioning as an acyclic directed graph of PDUs. For backfilling purposes, this is done on a per context basis.
|
||||
When we send a PDU we include all PDUs that have been received for that context that hasn't been subsequently listed in a later PDU. The trivial case is a simple list of PDUs, e.g. A <- B <- C. However, if two servers send out a PDU at the same to, both B and C would point at A - a later PDU would then list both B and C.
|
||||
|
||||
Problems with opaque version strings:
|
||||
- How do you do clustering without mandating that a cluster can only have one transaction in flight to a given remote home server at a time.
|
||||
If you have multiple transactions sent at once, then you might drop one transaction, receive another with a version that is later than the dropped transaction and which point ARGH WE LOST A TRANSACTION.
|
||||
- How do you do backfilling? A version string defines a point in a stream w.r.t. a single home server, not a point in the context.
|
||||
|
||||
We only need to store the ends of the directed graph, we DO NOT need to do the whole one table of nodes and one of edges.
|
||||
@@ -1,839 +0,0 @@
|
||||
Matrix Specification
|
||||
====================
|
||||
|
||||
TODO(Introduction) : Matthew
|
||||
- Similar to intro paragraph from README.
|
||||
- Explaining the overall mission, what this spec describes...
|
||||
- "What is Matrix?"
|
||||
- Draw parallels with email?
|
||||
|
||||
Architecture
|
||||
============
|
||||
|
||||
Clients transmit data to other clients through home servers (HSes). Clients do not communicate with each
|
||||
other directly.
|
||||
|
||||
::
|
||||
|
||||
How data flows between clients
|
||||
==============================
|
||||
|
||||
{ Matrix client A } { Matrix client B }
|
||||
^ | ^ |
|
||||
| events | | events |
|
||||
| V | V
|
||||
+------------------+ +------------------+
|
||||
| |---------( HTTP )---------->| |
|
||||
| Home Server | | Home Server |
|
||||
| |<--------( HTTP )-----------| |
|
||||
+------------------+ Federation +------------------+
|
||||
|
||||
A "Client" is an end-user, typically a human using a web application or mobile app. Clients use the
|
||||
"Client-to-Server" (C-S) API to communicate with their home server. A single Client is usually
|
||||
responsible for a single user account. A user account is represented by their "User ID". This ID is
|
||||
namespaced to the home server which allocated the account and looks like::
|
||||
|
||||
@localpart:domain
|
||||
|
||||
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user.
|
||||
|
||||
|
||||
A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes.
|
||||
It is typically responsible for multiple clients. "Federation" is the term used to describe the
|
||||
sharing of data between two or more home servers.
|
||||
|
||||
Data in Matrix is encapsulated in an "Event". An event is an action within the system. Typically each
|
||||
action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is
|
||||
used to differentiate different kinds of data. ``type`` values SHOULD be namespaced according to standard
|
||||
Java package naming conventions, e.g. ``com.example.myapp.event``. Events are usually sent in the context
|
||||
of a "Room".
|
||||
|
||||
Room structure
|
||||
--------------
|
||||
|
||||
A room is a conceptual place where users can send and receive events. Rooms
|
||||
can be created, joined and left. Events are sent to a room, and all
|
||||
participants in that room will receive the event. Rooms are uniquely
|
||||
identified via a "Room ID", which look like::
|
||||
|
||||
!opaque_id:domain
|
||||
|
||||
There is exactly one room ID for each room. Whilst the room ID does contain a
|
||||
domain, it is simply for namespacing room IDs. The room does NOT reside on the
|
||||
domain specified. Room IDs are not meant to be human readable.
|
||||
|
||||
The following diagram shows an ``m.room.message`` event being sent in the room
|
||||
``!qporfwt:matrix.org``::
|
||||
|
||||
{ @alice:matrix.org } { @bob:domain.com }
|
||||
| ^
|
||||
| |
|
||||
Room ID: !qporfwt:matrix.org Room ID: !qporfwt:matrix.org
|
||||
Event type: m.room.message Event type: m.room.message
|
||||
Content: { JSON object } Content: { JSON object }
|
||||
| |
|
||||
V |
|
||||
+------------------+ +------------------+
|
||||
| Home Server | | Home Server |
|
||||
| matrix.org |<-------Federation------->| domain.com |
|
||||
+------------------+ +------------------+
|
||||
| ................................. |
|
||||
|______| Partially Shared State |_______|
|
||||
| Room ID: !qporfwt:matrix.org |
|
||||
| Servers: matrix.org, domain.com |
|
||||
| Members: |
|
||||
| - @alice:matrix.org |
|
||||
| - @bob:domain.com |
|
||||
|.................................|
|
||||
|
||||
Federation maintains shared state between multiple home servers, such that when an event is
|
||||
sent to a room, the home server knows where to forward the event on to, and how to process
|
||||
the event. Home servers do not need to have completely shared state in order to participate
|
||||
in a room. State is scoped to a single room, and federation ensures that all home servers
|
||||
have the information they need, even if that means the home server has to request more
|
||||
information from another home server before processing the event.
|
||||
|
||||
Room Aliases
|
||||
------------
|
||||
|
||||
Each room can also have multiple "Room Aliases", which looks like::
|
||||
|
||||
#room_alias:domain
|
||||
|
||||
A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained
|
||||
by visiting the domain specified. Room aliases are designed to be human readable strings
|
||||
which can be used to publicise rooms. Note that the mapping from a room alias to a
|
||||
room ID is not fixed, and may change over time to point to a different room ID. For this
|
||||
reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on
|
||||
subsequent requests.
|
||||
|
||||
::
|
||||
|
||||
GET
|
||||
#matrix:domain.com !aaabaa:matrix.org
|
||||
| ^
|
||||
| |
|
||||
_______V____________________|____
|
||||
| domain.com |
|
||||
| Mappings: |
|
||||
| #matrix >> !aaabaa:matrix.org |
|
||||
| #golf >> !wfeiofh:sport.com |
|
||||
| #bike >> !4rguxf:matrix.org |
|
||||
|________________________________|
|
||||
|
||||
|
||||
Identity
|
||||
--------
|
||||
- Identity in relation to 3PIDs. Discovery of users based on 3PIDs.
|
||||
- Identity servers; trusted clique of servers which replicate content.
|
||||
- They govern the mapping of 3PIDs to user IDs and the creation of said mappings.
|
||||
- Not strictly required in order to communicate.
|
||||
|
||||
|
||||
API Standards
|
||||
-------------
|
||||
- All HTTP[S]
|
||||
- Uses JSON as HTTP bodies
|
||||
- Standard error response format { errcode: M_WHATEVER, error: "some message" }
|
||||
- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
|
||||
|
||||
Receiving live updates on a client
|
||||
----------------------------------
|
||||
- C-S longpoll event stream
|
||||
- Concept of start/end tokens.
|
||||
- Mention /initialSync to get token.
|
||||
|
||||
|
||||
Rooms
|
||||
=====
|
||||
- How are they created? PDU anchor point: "root of the tree".
|
||||
- Adding / removing aliases.
|
||||
- Invite/join dance
|
||||
- State and non-state data (+extensibility)
|
||||
|
||||
TODO : Room permissions / config / power levels.
|
||||
|
||||
Messages
|
||||
========
|
||||
|
||||
This specification outlines several standard event types, all of which are
|
||||
prefixed with ``m.``
|
||||
|
||||
State messages
|
||||
--------------
|
||||
- m.room.name
|
||||
- m.room.topic
|
||||
- m.room.member
|
||||
- m.room.config
|
||||
- m.room.invite_join
|
||||
|
||||
What are they, when are they used, what do they contain, how should they be used
|
||||
|
||||
Non-state messages
|
||||
------------------
|
||||
- m.room.message
|
||||
- m.room.message.feedback (and compressed format)
|
||||
|
||||
What are they, when are they used, what do they contain, how should they be used
|
||||
|
||||
m.room.message msgtypes
|
||||
-----------------------
|
||||
Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
|
||||
message being sent. Each type has their own required and optional keys, as outlined
|
||||
below:
|
||||
|
||||
``m.text``
|
||||
Required keys:
|
||||
- ``body`` : "string" - The body of the message.
|
||||
Optional keys:
|
||||
None.
|
||||
Example:
|
||||
``{ "msgtype": "m.text", "body": "I am a fish" }``
|
||||
|
||||
``m.emote``
|
||||
Required keys:
|
||||
- ``body`` : "string" - The emote action to perform.
|
||||
Optional keys:
|
||||
None.
|
||||
Example:
|
||||
``{ "msgtype": "m.emote", "body": "tries to come up with a witty explanation" }``
|
||||
|
||||
``m.image``
|
||||
Required keys:
|
||||
- ``url`` : "string" - The URL to the image.
|
||||
Optional keys:
|
||||
- ``info`` : "string" - info : JSON object (ImageInfo) - The image info for image
|
||||
referred to in ``url``.
|
||||
- ``thumbnail_url`` : "string" - The URL to the thumbnail.
|
||||
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
||||
referred to in ``thumbnail_url``.
|
||||
- ``body`` : "string" - The alt text of the image, or some kind of content
|
||||
description for accessibility e.g. "image attachment".
|
||||
|
||||
ImageInfo:
|
||||
Information about an image::
|
||||
|
||||
{
|
||||
"size" : integer (size of image in bytes),
|
||||
"w" : integer (width of image in pixels),
|
||||
"h" : integer (height of image in pixels),
|
||||
"mimetype" : "string (e.g. image/jpeg)",
|
||||
}
|
||||
|
||||
``m.audio``
|
||||
Required keys:
|
||||
- ``url`` : "string" - The URL to the audio.
|
||||
Optional keys:
|
||||
- ``info`` : JSON object (AudioInfo) - The audio info for the audio referred to in
|
||||
``url``.
|
||||
- ``body`` : "string" - A description of the audio e.g. "Bee Gees -
|
||||
Stayin' Alive", or some kind of content description for accessibility e.g.
|
||||
"audio attachment".
|
||||
AudioInfo:
|
||||
Information about a piece of audio::
|
||||
|
||||
{
|
||||
"mimetype" : "string (e.g. audio/aac)",
|
||||
"size" : integer (size of audio in bytes),
|
||||
"duration" : integer (duration of audio in milliseconds),
|
||||
}
|
||||
|
||||
``m.video``
|
||||
Required keys:
|
||||
- ``url`` : "string" - The URL to the video.
|
||||
Optional keys:
|
||||
- ``info`` : JSON object (VideoInfo) - The video info for the video referred to in
|
||||
``url``.
|
||||
- ``body`` : "string" - A description of the video e.g. "Gangnam style",
|
||||
or some kind of content description for accessibility e.g. "video attachment".
|
||||
|
||||
VideoInfo:
|
||||
Information about a video::
|
||||
|
||||
{
|
||||
"mimetype" : "string (e.g. video/mp4)",
|
||||
"size" : integer (size of video in bytes),
|
||||
"duration" : integer (duration of video in milliseconds),
|
||||
"w" : integer (width of video in pixels),
|
||||
"h" : integer (height of video in pixels),
|
||||
"thumbnail_url" : "string (URL to image)",
|
||||
"thumbanil_info" : JSON object (ImageInfo)
|
||||
}
|
||||
|
||||
``m.location``
|
||||
Required keys:
|
||||
- ``geo_uri`` : "string" - The geo URI representing the location.
|
||||
Optional keys:
|
||||
- ``thumbnail_url`` : "string" - The URL to a thumnail of the location being
|
||||
represented.
|
||||
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
||||
referred to in ``thumbnail_url``.
|
||||
- ``body`` : "string" - A description of the location e.g. "Big Ben,
|
||||
London, UK", or some kind of content description for accessibility e.g.
|
||||
"location attachment".
|
||||
|
||||
The following keys can be attached to any ``m.room.message``:
|
||||
|
||||
Optional keys:
|
||||
- ``sender_ts`` : integer - A timestamp (ms resolution) representing the
|
||||
wall-clock time when the message was sent from the client.
|
||||
|
||||
Presence
|
||||
========
|
||||
|
||||
Each user has the concept of presence information. This encodes the
|
||||
"availability" of that user, suitable for display on other user's clients. This
|
||||
is transmitted as an ``m.presence`` event and is one of the few events which
|
||||
are sent *outside the context of a room*. The basic piece of presence information
|
||||
is represented by the ``state`` key, which is an enum of one of the following:
|
||||
|
||||
- ``online`` : The default state when the user is connected to an event stream.
|
||||
- ``unavailable`` : The user is not reachable at this time.
|
||||
- ``offline`` : The user is not connected to an event stream.
|
||||
- ``free_for_chat`` : The user is generally willing to receive messages
|
||||
moreso than default.
|
||||
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
|
||||
state anyway and generally interact with client features.
|
||||
|
||||
This basic ``state`` field applies to the user as a whole, regardless of how many
|
||||
client devices they have connected. The home server should synchronise this
|
||||
status choice among multiple devices to ensure the user gets a consistent
|
||||
experience.
|
||||
|
||||
Idle Time
|
||||
---------
|
||||
As well as the basic ``state`` field, the presence information can also show a sense
|
||||
of an "idle timer". This should be maintained individually by the user's
|
||||
clients, and the home server can take the highest reported time as that to
|
||||
report. When a user is offline, the home server can still report when the user was last
|
||||
seen online.
|
||||
|
||||
Transmission
|
||||
------------
|
||||
- Transmitted as an EDU.
|
||||
- Presence lists determine who to send to.
|
||||
|
||||
Presence List
|
||||
-------------
|
||||
Each user's home server stores a "presence list" for that user. This stores a
|
||||
list of other user IDs the user has chosen to add to it. To be added to this
|
||||
list, the user being added must receive permission from the list owner. Once
|
||||
granted, both user's HS(es) store this information. Since such subscriptions
|
||||
are likely to be bidirectional, HSes may wish to automatically accept requests
|
||||
when a reverse subscription already exists.
|
||||
|
||||
Presence and Permissions
|
||||
------------------------
|
||||
For a viewing user to be allowed to see the presence information of a target
|
||||
user, either:
|
||||
|
||||
- The target user has allowed the viewing user to add them to their presence
|
||||
list, or
|
||||
- The two users share at least one room in common
|
||||
|
||||
In the latter case, this allows for clients to display some minimal sense of
|
||||
presence information in a user list for a room.
|
||||
|
||||
Typing notifications
|
||||
====================
|
||||
|
||||
TODO : Leo
|
||||
|
||||
Voice over IP
|
||||
=============
|
||||
|
||||
TODO : Dave
|
||||
|
||||
Profiles
|
||||
========
|
||||
|
||||
Internally within Matrix users are referred to by their user ID, which is not a
|
||||
human-friendly string. Profiles grant users the ability to see human-readable
|
||||
names for other users that are in some way meaningful to them. Additionally,
|
||||
profiles can publish additional information, such as the user's age or location.
|
||||
|
||||
A Profile consists of a display name, an avatar picture, and a set of other
|
||||
metadata fields that the user may wish to publish (email address, phone
|
||||
numbers, website URLs, etc...). This specification puts no requirements on the
|
||||
display name other than it being a valid unicode string.
|
||||
|
||||
- Metadata extensibility
|
||||
- Bundled with which events? e.g. m.room.member
|
||||
- Generate own events? What type?
|
||||
|
||||
Registration and login
|
||||
======================
|
||||
|
||||
Clients must register with a home server in order to use Matrix. After
|
||||
registering, the client will be given an access token which must be used in ALL
|
||||
requests to that home server as a query parameter 'access_token'.
|
||||
|
||||
- TODO Kegan : Make registration like login (just omit the "user" key on the
|
||||
initial request?)
|
||||
|
||||
If the client has already registered, they need to be able to login to their
|
||||
account. The home server may provide many different ways of logging in, such
|
||||
as user/password auth, login via a social network (OAuth2), login by confirming
|
||||
a token sent to their email address, etc. This specification does not define how
|
||||
home servers should authorise their users who want to login to their existing
|
||||
accounts, but instead defines the standard interface which implementations
|
||||
should follow so that ANY client can login to ANY home server.
|
||||
|
||||
The login process breaks down into the following:
|
||||
1. Determine the requirements for logging in.
|
||||
2. Submit the login stage credentials.
|
||||
3. Get credentials or be told the next stage in the login process and repeat
|
||||
step 2.
|
||||
|
||||
As each home server may have different ways of logging in, the client needs to know how
|
||||
they should login. All distinct login stages MUST have a corresponding ``type``.
|
||||
A ``type`` is a namespaced string which details the mechanism for logging in.
|
||||
|
||||
A client may be able to login via multiple valid login flows, and should choose a single
|
||||
flow when logging in. A flow is a series of login stages. The home server MUST respond
|
||||
with all the valid login flows when requested::
|
||||
|
||||
The client can login via 3 paths: 1a and 1b, 2a and 2b, or 3. The client should
|
||||
select one of these paths.
|
||||
|
||||
{
|
||||
"flows": [
|
||||
{
|
||||
"type": "<login type1a>",
|
||||
"stages": [ "<login type 1a>", "<login type 1b>" ]
|
||||
},
|
||||
{
|
||||
"type": "<login type2a>",
|
||||
"stages": [ "<login type 2a>", "<login type 2b>" ]
|
||||
},
|
||||
{
|
||||
"type": "<login type3>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
After the login is completed, the client's fully-qualified user ID and a new access
|
||||
token MUST be returned::
|
||||
|
||||
{
|
||||
"user_id": "@user:matrix.org",
|
||||
"access_token": "abcdef0123456789"
|
||||
}
|
||||
|
||||
The ``user_id`` key is particularly useful if the home server wishes to support
|
||||
localpart entry of usernames (e.g. "user" rather than "@user:matrix.org"), as the
|
||||
client may not be able to determine its ``user_id`` in this case.
|
||||
|
||||
If a login has multiple requests, the home server may wish to create a session. If
|
||||
a home server responds with a 'session' key to a request, clients MUST submit it in
|
||||
subsequent requests until the login is completed::
|
||||
|
||||
{
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
This specification defines the following login types:
|
||||
- ``m.login.password``
|
||||
- ``m.login.oauth2``
|
||||
- ``m.login.email.code``
|
||||
- ``m.login.email.url``
|
||||
|
||||
|
||||
Password-based
|
||||
--------------
|
||||
:Type:
|
||||
m.login.password
|
||||
:Description:
|
||||
Login is supported via a username and password.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.password",
|
||||
"user": "<user_id or user localpart>",
|
||||
"password": "<password>"
|
||||
}
|
||||
|
||||
The home server MUST respond with either new credentials, the next stage of the login
|
||||
process, or a standard error response.
|
||||
|
||||
OAuth2-based
|
||||
------------
|
||||
:Type:
|
||||
m.login.oauth2
|
||||
:Description:
|
||||
Login is supported via OAuth2 URLs. This login consists of multiple requests.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.oauth2",
|
||||
"user": "<user_id or user localpart>"
|
||||
}
|
||||
|
||||
The server MUST respond with::
|
||||
|
||||
{
|
||||
"uri": <Authorization Request URI OR service selection URI>
|
||||
}
|
||||
|
||||
The home server acts as a 'confidential' client for the purposes of OAuth2.
|
||||
If the uri is a ``sevice selection URI``, it MUST point to a webpage which prompts the
|
||||
user to choose which service to authorize with. On selection of a service, this
|
||||
MUST link through to an ``Authorization Request URI``. If there is only 1 service which the
|
||||
home server accepts when logging in, this indirection can be skipped and the
|
||||
"uri" key can be the ``Authorization Request URI``.
|
||||
|
||||
The client then visits the ``Authorization Request URI``, which then shows the OAuth2
|
||||
Allow/Deny prompt. Hitting 'Allow' returns the ``redirect URI`` with the auth code.
|
||||
Home servers can choose any path for the ``redirect URI``. The client should visit
|
||||
the ``redirect URI``, which will then finish the OAuth2 login process, granting the
|
||||
home server an access token for the chosen service. When the home server gets
|
||||
this access token, it verifies that the cilent has authorised with the 3rd party, and
|
||||
can now complete the login. The OAuth2 ``redirect URI`` (with auth code) MUST respond
|
||||
with either new credentials, the next stage of the login process, or a standard error
|
||||
response.
|
||||
|
||||
For example, if a home server accepts OAuth2 from Google, it would return the
|
||||
Authorization Request URI for Google::
|
||||
|
||||
{
|
||||
"uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
|
||||
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos"
|
||||
}
|
||||
|
||||
The client then visits this URI and authorizes the home server. The client then
|
||||
visits the REDIRECT_URI with the auth code= query parameter which returns::
|
||||
|
||||
{
|
||||
"user_id": "@user:matrix.org",
|
||||
"access_token": "0123456789abcdef"
|
||||
}
|
||||
|
||||
Email-based (code)
|
||||
------------------
|
||||
:Type:
|
||||
m.login.email.code
|
||||
:Description:
|
||||
Login is supported by typing in a code which is sent in an email. This login
|
||||
consists of multiple requests.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.email.code",
|
||||
"user": "<user_id or user localpart>",
|
||||
"email": "<email address>"
|
||||
}
|
||||
|
||||
After validating the email address, the home server MUST send an email containing
|
||||
an authentication code and return::
|
||||
|
||||
{
|
||||
"type": "m.login.email.code",
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
The second request in this login stage involves sending this authentication code::
|
||||
|
||||
{
|
||||
"type": "m.login.email.code",
|
||||
"session": "<session id>",
|
||||
"code": "<code in email sent>"
|
||||
}
|
||||
|
||||
The home server MUST respond to this with either new credentials, the next stage of
|
||||
the login process, or a standard error response.
|
||||
|
||||
Email-based (url)
|
||||
-----------------
|
||||
:Type:
|
||||
m.login.email.url
|
||||
:Description:
|
||||
Login is supported by clicking on a URL in an email. This login consists of
|
||||
multiple requests.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.email.url",
|
||||
"user": "<user_id or user localpart>",
|
||||
"email": "<email address>"
|
||||
}
|
||||
|
||||
After validating the email address, the home server MUST send an email containing
|
||||
an authentication URL and return::
|
||||
|
||||
{
|
||||
"type": "m.login.email.url",
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
The email contains a URL which must be clicked. After it has been clicked, the
|
||||
client should perform another request::
|
||||
|
||||
{
|
||||
"type": "m.login.email.url",
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
The home server MUST respond to this with either new credentials, the next stage of
|
||||
the login process, or a standard error response.
|
||||
|
||||
A common client implementation will be to periodically poll until the link is clicked.
|
||||
If the link has not been visited yet, a standard error response with an errcode of
|
||||
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
|
||||
|
||||
|
||||
N-Factor Authentication
|
||||
-----------------------
|
||||
Multiple login stages can be combined to create N-factor authentication during login.
|
||||
|
||||
This can be achieved by responding with the ``next`` login type on completion of a
|
||||
previous login stage::
|
||||
|
||||
{
|
||||
"next": "<next login type>"
|
||||
}
|
||||
|
||||
If a home server implements N-factor authentication, it MUST respond with all
|
||||
``stages`` when initially queried for their login requirements::
|
||||
|
||||
{
|
||||
"type": "<1st login type>",
|
||||
"stages": [ <1st login type>, <2nd login type>, ... , <Nth login type> ]
|
||||
}
|
||||
|
||||
This can be represented conceptually as::
|
||||
|
||||
_______________________
|
||||
| Login Stage 1 |
|
||||
| type: "<login type1>" |
|
||||
| ___________________ |
|
||||
| |_Request_1_________| | <-- Returns "session" key which is used throughout.
|
||||
| ___________________ |
|
||||
| |_Request_2_________| | <-- Returns a "next" value of "login type2"
|
||||
|_______________________|
|
||||
|
|
||||
|
|
||||
_________V_____________
|
||||
| Login Stage 2 |
|
||||
| type: "<login type2>" |
|
||||
| ___________________ |
|
||||
| |_Request_1_________| |
|
||||
| ___________________ |
|
||||
| |_Request_2_________| |
|
||||
| ___________________ |
|
||||
| |_Request_3_________| | <-- Returns a "next" value of "login type3"
|
||||
|_______________________|
|
||||
|
|
||||
|
|
||||
_________V_____________
|
||||
| Login Stage 3 |
|
||||
| type: "<login type3>" |
|
||||
| ___________________ |
|
||||
| |_Request_1_________| | <-- Returns user credentials
|
||||
|_______________________|
|
||||
|
||||
Fallback
|
||||
--------
|
||||
Clients cannot be expected to be able to know how to process every single
|
||||
login type. If a client determines it does not know how to handle a given
|
||||
login type, it should request a login fallback page::
|
||||
|
||||
GET matrix/client/api/v1/login/fallback
|
||||
|
||||
This MUST return an HTML page which can perform the entire login process.
|
||||
|
||||
Identity
|
||||
========
|
||||
|
||||
TODO : Dave
|
||||
- 3PIDs and identity server, functions
|
||||
|
||||
Federation
|
||||
==========
|
||||
|
||||
Federation is the term used to describe how to communicate between Matrix home
|
||||
servers. Federation is a mechanism by which two home servers can exchange
|
||||
Matrix event messages, both as a real-time push of current events, and as a
|
||||
historic fetching mechanism to synchronise past history for clients to view. It
|
||||
uses HTTP connections between each pair of servers involved as the underlying
|
||||
transport. Messages are exchanged between servers in real-time by active pushing
|
||||
from each server's HTTP client into the server of the other. Queries to fetch
|
||||
historic data for the purpose of back-filling scrollback buffers and the like
|
||||
can also be performed.
|
||||
|
||||
There are three main kinds of communication that occur between home servers:
|
||||
|
||||
:Queries:
|
||||
These are single request/response interactions between a given pair of
|
||||
servers, initiated by one side sending an HTTP GET request to obtain some
|
||||
information, and responded by the other. They are not persisted and contain
|
||||
no long-term significant history. They simply request a snapshot state at the
|
||||
instant the query is made.
|
||||
|
||||
:Ephemeral Data Units (EDUs):
|
||||
These are notifications of events that are pushed from one home server to
|
||||
another. They are not persisted and contain no long-term significant history,
|
||||
nor does the receiving home server have to reply to them.
|
||||
|
||||
:Persisted Data Units (PDUs):
|
||||
These are notifications of events that are broadcast from one home server to
|
||||
any others that are interested in the same "context" (namely, a Room ID).
|
||||
They are persisted to long-term storage and form the record of history for
|
||||
that context.
|
||||
|
||||
EDUs and PDUs are further wrapped in an envelope called a Transaction, which is
|
||||
transferred from the origin to the destination home server using an HTTP PUT request.
|
||||
|
||||
|
||||
Transactions
|
||||
------------
|
||||
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
||||
of Transaction messages, which are encoded as JSON objects, passed over an
|
||||
HTTP PUT request. A Transaction is meaningful only to the pair of home servers that
|
||||
exchanged it; they are not globally-meaningful.
|
||||
|
||||
Each transaction has:
|
||||
- An opaque transaction ID.
|
||||
- A timestamp (UNIX epoch time in milliseconds) generated by its origin server.
|
||||
- An origin and destination server name.
|
||||
- A list of "previous IDs".
|
||||
- A list of PDUs and EDUs - the actual message payload that the Transaction carries.
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
||||
"ts":1404835423000,
|
||||
"origin":"red",
|
||||
"destination":"blue",
|
||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
||||
"pdus":[...],
|
||||
"edus":[...]
|
||||
}
|
||||
|
||||
The ``prev_ids`` field contains a list of previous transaction IDs that
|
||||
the ``origin`` server has sent to this ``destination``. Its purpose is to act as a
|
||||
sequence checking mechanism - the destination server can check whether it has
|
||||
successfully received that Transaction, or ask for a retransmission if not.
|
||||
|
||||
The ``pdus`` field of a transaction is a list, containing zero or more PDUs.[*]
|
||||
Each PDU is itself a JSON object containing a number of keys, the exact details of
|
||||
which will vary depending on the type of PDU. Similarly, the ``edus`` field is
|
||||
another list containing the EDUs. This key may be entirely absent if there are
|
||||
no EDUs to transfer.
|
||||
|
||||
(* Normally the PDU list will be non-empty, but the server should cope with
|
||||
receiving an "empty" transaction, as this is useful for informing peers of other
|
||||
transaction IDs they should be aware of. This effectively acts as a push
|
||||
mechanism to encourage peers to continue to replicate content.)
|
||||
|
||||
PDUs and EDUs
|
||||
-------------
|
||||
|
||||
All PDUs have:
|
||||
- An ID
|
||||
- A context
|
||||
- A declaration of their type
|
||||
- A list of other PDU IDs that have been seen recently on that context (regardless of which origin
|
||||
sent them)
|
||||
|
||||
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
|
||||
[origin,ref] pair like the prev_pdus are]]
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"pdu_id":"a4ecee13e2accdadf56c1025af232176",
|
||||
"context":"#example.green",
|
||||
"origin":"green",
|
||||
"ts":1404838188000,
|
||||
"pdu_type":"m.text",
|
||||
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
|
||||
"content":...
|
||||
"is_state":false
|
||||
}
|
||||
|
||||
In contrast to Transactions, it is important to note that the ``prev_pdus``
|
||||
field of a PDU refers to PDUs that any origin server has sent, rather than
|
||||
previous IDs that this ``origin`` has sent. This list may refer to other PDUs sent
|
||||
by the same origin as the current one, or other origins.
|
||||
|
||||
Because of the distributed nature of participants in a Matrix conversation, it
|
||||
is impossible to establish a globally-consistent total ordering on the events.
|
||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
||||
has received, a partial ordering can be constructed allowing causallity
|
||||
relationships to be preserved. A client can then display these messages to the
|
||||
end-user in some order consistent with their content and ensure that no message
|
||||
that is semantically in reply of an earlier one is ever displayed before it.
|
||||
|
||||
PDUs fall into two main categories: those that deliver Events, and those that
|
||||
synchronise State. For PDUs that relate to State synchronisation, additional
|
||||
keys exist to support this:
|
||||
|
||||
::
|
||||
|
||||
{...,
|
||||
"is_state":true,
|
||||
"state_key":TODO
|
||||
"power_level":TODO
|
||||
"prev_state_id":TODO
|
||||
"prev_state_origin":TODO}
|
||||
|
||||
[[TODO(paul): At this point we should probably have a long description of how
|
||||
State management works, with descriptions of clobbering rules, power levels, etc
|
||||
etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
|
||||
so on. This part needs refining. And writing in its own document as the details
|
||||
relate to the server/system as a whole, not specifically to server-server
|
||||
federation.]]
|
||||
|
||||
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
||||
"previous" IDs. The only mandatory fields for these are the type, origin and
|
||||
destination home server names, and the actual nested content.
|
||||
|
||||
::
|
||||
|
||||
{"edu_type":"m.presence",
|
||||
"origin":"blue",
|
||||
"destination":"orange",
|
||||
"content":...}
|
||||
|
||||
Backfilling
|
||||
-----------
|
||||
- What it is, when is it used, how is it done
|
||||
|
||||
SRV Records
|
||||
-----------
|
||||
- Why it is needed
|
||||
|
||||
Security
|
||||
========
|
||||
- rate limiting
|
||||
- crypto (s-s auth)
|
||||
- E2E
|
||||
- Lawful intercept + Key Escrow
|
||||
|
||||
TODO Mark
|
||||
|
||||
Policy Servers
|
||||
==============
|
||||
TODO
|
||||
|
||||
Content repository
|
||||
==================
|
||||
- thumbnail paths
|
||||
|
||||
Address book repository
|
||||
=======================
|
||||
- format
|
||||
|
||||
|
||||
Glossary
|
||||
========
|
||||
- domain specific words/acronyms with definitions
|
||||
|
||||
User ID:
|
||||
An opaque ID which identifies an end-user, which consists of some opaque
|
||||
localpart combined with the domain name of their home server.
|
||||
1
docs/sphinx/README.rst
Normal file
1
docs/sphinx/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
TODO: how (if at all) is this actually maintained?
|
||||
93
docs/turn-howto.rst
Normal file
93
docs/turn-howto.rst
Normal file
@@ -0,0 +1,93 @@
|
||||
How to enable VoIP relaying on your Home Server with TURN
|
||||
|
||||
Overview
|
||||
--------
|
||||
The synapse Matrix Home Server supports integration with TURN server via the
|
||||
TURN server REST API
|
||||
(http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00). This allows
|
||||
the Home Server to generate credentials that are valid for use on the TURN
|
||||
server through the use of a secret shared between the Home Server and the
|
||||
TURN server.
|
||||
|
||||
This document described how to install coturn
|
||||
(https://code.google.com/p/coturn/) which also supports the TURN REST API,
|
||||
and integrate it with synapse.
|
||||
|
||||
coturn Setup
|
||||
============
|
||||
|
||||
1. Check out coturn::
|
||||
svn checkout http://coturn.googlecode.com/svn/trunk/ coturn
|
||||
cd coturn
|
||||
|
||||
2. Configure it::
|
||||
./configure
|
||||
|
||||
You may need to install libevent2: if so, you should do so
|
||||
in the way recommended by your operating system.
|
||||
You can ignore warnings about lack of database support: a
|
||||
database is unnecessary for this purpose.
|
||||
|
||||
3. Build and install it::
|
||||
make
|
||||
make install
|
||||
|
||||
4. Make a config file in /etc/turnserver.conf. You can customise
|
||||
a config file from turnserver.conf.default. The relevant
|
||||
lines, with example values, are::
|
||||
|
||||
lt-cred-mech
|
||||
use-auth-secret
|
||||
static-auth-secret=[your secret key here]
|
||||
realm=turn.myserver.org
|
||||
|
||||
See turnserver.conf.default for explanations of the options.
|
||||
One way to generate the static-auth-secret is with pwgen::
|
||||
|
||||
pwgen -s 64 1
|
||||
|
||||
5. Ensure youe firewall allows traffic into the TURN server on
|
||||
the ports you've configured it to listen on (remember to allow
|
||||
both TCP and UDP if you've enabled both).
|
||||
|
||||
6. If you've configured coturn to support TLS/DTLS, generate or
|
||||
import your private key and certificate.
|
||||
|
||||
7. Start the turn server::
|
||||
bin/turnserver -o
|
||||
|
||||
|
||||
synapse Setup
|
||||
=============
|
||||
|
||||
Your home server configuration file needs the following extra keys:
|
||||
|
||||
1. "turn_uris": This needs to be a yaml list
|
||||
of public-facing URIs for your TURN server to be given out
|
||||
to your clients. Add separate entries for each transport your
|
||||
TURN server supports.
|
||||
|
||||
2. "turn_shared_secret": This is the secret shared between your Home
|
||||
server and your TURN server, so you should set it to the same
|
||||
string you used in turnserver.conf.
|
||||
|
||||
3. "turn_user_lifetime": This is the amount of time credentials
|
||||
generated by your Home Server are valid for (in milliseconds).
|
||||
Shorter times offer less potential for abuse at the expense
|
||||
of increased traffic between web clients and your home server
|
||||
to refresh credentials. The TURN REST API specification recommends
|
||||
one day (86400000).
|
||||
|
||||
As an example, here is the relevant section of the config file for
|
||||
matrix.org::
|
||||
|
||||
turn_uris: turn:turn.matrix.org:3478?transport=udp,turn:turn.matrix.org:3478?transport=tcp
|
||||
turn_shared_secret: n0t4ctuAllymatr1Xd0TorgSshar3d5ecret4obvIousreAsons
|
||||
turn_user_lifetime: 86400000
|
||||
|
||||
Now, restart synapse::
|
||||
|
||||
cd /where/you/run/synapse
|
||||
./synctl restart
|
||||
|
||||
...and your Home Server now supports VoIP relaying!
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -120,7 +120,7 @@ def make_graph(pdus, room, filename_prefix):
|
||||
def get_pdus(host, room):
|
||||
transaction = json.loads(
|
||||
urllib2.urlopen(
|
||||
"http://%s/matrix/federation/v1/context/%s/" % (host, room)
|
||||
"http://%s/_matrix/federation/v1/context/%s/" % (host, room)
|
||||
).read()
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8080</p>
|
||||
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
|
||||
@@ -10,7 +10,7 @@ $('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
@@ -19,17 +19,23 @@ $('.login').live('click', function() {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=0; i<data.length; ++i) {
|
||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
||||
addRoom(data[i]);
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
addRoom(rooms[i]);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
@@ -43,7 +49,7 @@ $('.createRoom').live('click', function() {
|
||||
data.room_alias_name = roomAlias;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
@@ -78,11 +84,9 @@ $('.sendMessage').live('click', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
||||
url = url.replace("$msgid", msgId);
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
@@ -91,7 +95,7 @@ $('.sendMessage').live('click', function() {
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "PUT",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<p>This event stream demo requires a home server to be running on http://localhost:8080</p>
|
||||
<p>This event stream demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
|
||||
@@ -7,7 +7,7 @@ var eventStreamInfo = {
|
||||
var roomInfo = [];
|
||||
|
||||
var longpollEventStream = function() {
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$from", eventStreamInfo.from);
|
||||
|
||||
@@ -48,7 +48,7 @@ $('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
@@ -58,21 +58,27 @@ $('.login').live('click', function() {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
$("#roomId").val("");
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=0; i<data.length; ++i) {
|
||||
if ("messages" in data[i]) {
|
||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
if ("messages" in rooms[i]) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
}
|
||||
}
|
||||
roomInfo = data;
|
||||
roomInfo = rooms;
|
||||
setRooms(roomInfo);
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
@@ -92,17 +98,14 @@ $('.sendMessage').live('click', function() {
|
||||
|
||||
var sendMessage = function(roomId) {
|
||||
var body = "jsfiddle message @" + $.now();
|
||||
var msgId = $.now();
|
||||
|
||||
if (roomId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
||||
url = url.replace("$msgid", msgId);
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
@@ -111,7 +114,7 @@ var sendMessage = function(roomId) {
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "PUT",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
|
||||
7
jsfiddles/example_app/demo.details
Normal file
7
jsfiddles/example_app/demo.details
Normal file
@@ -0,0 +1,7 @@
|
||||
name: Example Matrix Client
|
||||
description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
|
||||
authors:
|
||||
- matrix.org
|
||||
resources:
|
||||
- http://matrix.org
|
||||
normalize_css: no
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="signUp">
|
||||
<p>Matrix example application: Requires a local home server running at http://localhost:8080</p>
|
||||
<p>Matrix example application: Requires a local home server running at http://localhost:8008</p>
|
||||
<form class="registrationForm">
|
||||
<p>No account? Register:</p>
|
||||
<input type="text" id="userReg" placeholder="Username"></input>
|
||||
|
||||
@@ -10,7 +10,7 @@ var viewingRoomId;
|
||||
|
||||
// ************** Event Streaming **************
|
||||
var longpollEventStream = function() {
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$from", eventStreamInfo.from);
|
||||
|
||||
@@ -38,8 +38,9 @@ var longpollEventStream = function() {
|
||||
else if (data.chunk[i].type === "m.room.member") {
|
||||
if (viewingRoomId === data.chunk[i].room_id) {
|
||||
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
||||
addMessage(data.chunk[i]);
|
||||
for (j=0; j<memberInfo.length; ++j) {
|
||||
if (memberInfo[j].target_user_id === data.chunk[i].target_user_id) {
|
||||
if (memberInfo[j].state_key === data.chunk[i].state_key) {
|
||||
memberInfo[j] = data.chunk[i];
|
||||
updatedMemberList = true;
|
||||
break;
|
||||
@@ -50,7 +51,7 @@ var longpollEventStream = function() {
|
||||
updatedMemberList = true;
|
||||
}
|
||||
}
|
||||
if (data.chunk[i].target_user_id === accountInfo.user_id) {
|
||||
if (data.chunk[i].state_key === accountInfo.user_id) {
|
||||
getCurrentRoomList(); // update our join/invite list
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,7 @@ $('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
@@ -106,10 +107,10 @@ $('.register').live('click', function() {
|
||||
var user = $("#userReg").val();
|
||||
var password = $("#passwordReg").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/register",
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user_id: user, password: password }),
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
onLoggedIn(data);
|
||||
@@ -133,7 +134,7 @@ $('.createRoom').live('click', function() {
|
||||
data.room_alias_name = roomAlias;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
@@ -154,14 +155,15 @@ $('.createRoom').live('click', function() {
|
||||
|
||||
// ************** Getting current state **************
|
||||
var getCurrentRoomList = function() {
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=0; i<data.length; ++i) {
|
||||
if ("messages" in data[i]) {
|
||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
if ("messages" in rooms[i]) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
}
|
||||
}
|
||||
roomInfo = data;
|
||||
roomInfo = rooms;
|
||||
setRooms(roomInfo);
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
@@ -179,7 +181,8 @@ var loadRoomContent = function(roomId) {
|
||||
|
||||
var getMessages = function(roomId) {
|
||||
$("#messages").empty();
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/messages/list?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=10";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
|
||||
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=data.chunk.length-1; i>=0; --i) {
|
||||
addMessage(data.chunk[i]);
|
||||
@@ -190,7 +193,8 @@ var getMessages = function(roomId) {
|
||||
var getMemberList = function(roomId) {
|
||||
$("#members").empty();
|
||||
memberInfo = [];
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/members/list?access_token=" + accountInfo.access_token;
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
|
||||
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=0; i<data.chunk.length; ++i) {
|
||||
memberInfo.push(data.chunk[i]);
|
||||
@@ -212,11 +216,9 @@ $('.sendMessage').live('click', function() {
|
||||
var sendMessage = function(roomId, body) {
|
||||
var msgId = $.now();
|
||||
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
||||
url = url.replace("$msgid", msgId);
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
@@ -225,7 +227,7 @@ var sendMessage = function(roomId, body) {
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "PUT",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
@@ -260,13 +262,12 @@ var setRooms = function(roomList) {
|
||||
var membership = $(this).find('td:eq(1)').text();
|
||||
if (membership !== "join") {
|
||||
console.log("Joining room " + roomId);
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "PUT",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({membership: "join"}),
|
||||
dataType: "json",
|
||||
@@ -286,16 +287,39 @@ var setRooms = function(roomList) {
|
||||
};
|
||||
|
||||
var addMessage = function(data) {
|
||||
|
||||
var msg = data.content.body;
|
||||
if (data.type === "m.room.member") {
|
||||
if (data.content.membership === undefined) {
|
||||
return;
|
||||
}
|
||||
if (data.content.membership === "invite") {
|
||||
msg = "<em>invited " + data.state_key + " to the room</em>";
|
||||
}
|
||||
else if (data.content.membership === "join") {
|
||||
msg = "<em>joined the room</em>";
|
||||
}
|
||||
else if (data.content.membership === "leave") {
|
||||
msg = "<em>left the room</em>";
|
||||
}
|
||||
else if (data.content.membership === "ban") {
|
||||
msg = "<em>was banned from the room</em>";
|
||||
}
|
||||
}
|
||||
if (msg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var row = "<tr>" +
|
||||
"<td>"+data.user_id+"</td>" +
|
||||
"<td>"+data.content.body+"</td>" +
|
||||
"<td>"+msg+"</td>" +
|
||||
"</tr>";
|
||||
$("#messages").append(row);
|
||||
};
|
||||
|
||||
var addMember = function(data) {
|
||||
var row = "<tr>" +
|
||||
"<td>"+data.target_user_id+"</td>" +
|
||||
"<td>"+data.state_key+"</td>" +
|
||||
"<td>"+data.content.membership+"</td>" +
|
||||
"</tr>";
|
||||
$("#members").append(row);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<p>This registration/login demo requires a home server to be running on http://localhost:8080</p>
|
||||
<p>This registration/login demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="registrationForm">
|
||||
<input type="text" id="user" placeholder="Username"></input>
|
||||
|
||||
@@ -11,23 +11,7 @@ $('.register').live('click', function() {
|
||||
var user = $("#user").val();
|
||||
var password = $("#password").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/register",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user_id: user, password: password }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var login = function(user, password) {
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
@@ -36,7 +20,33 @@ var login = function(user, password) {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var login = function(user, password) {
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -44,8 +54,8 @@ var login = function(user, password) {
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) {
|
||||
if (data.type !== "m.login.password") {
|
||||
$.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) {
|
||||
if (data.flows[0].type !== "m.login.password") {
|
||||
alert("I don't know how to login with this type: " + data.type);
|
||||
return;
|
||||
}
|
||||
@@ -60,7 +70,7 @@ $('.logout').live('click', function() {
|
||||
});
|
||||
|
||||
$('.testToken').live('click', function() {
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
||||
}).fail(function(err) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<p>This room membership demo requires a home server to be running on http://localhost:8080</p>
|
||||
<p>This room membership demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
@@ -14,9 +14,9 @@
|
||||
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
||||
<select id="membership">
|
||||
<option value="invite">Invite</option>
|
||||
<option value="join">Join</option>
|
||||
<option value="leave">Leave</option>
|
||||
<option value="invite">invite</option>
|
||||
<option value="join">join</option>
|
||||
<option value="leave">leave</option>
|
||||
</select>
|
||||
<input type="button" class="changeMembership" value="Change Membership"></input>
|
||||
</form>
|
||||
|
||||
@@ -4,13 +4,21 @@ var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
getCurrentRoomList();
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
$("#membership").change(function() {
|
||||
if ($("#membership").val() === "invite") {
|
||||
$("#targetUser").css({visibility: "visible"});
|
||||
}
|
||||
else {
|
||||
$("#targetUser").css({visibility: "hidden"});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
@@ -20,7 +28,12 @@ $('.login').live('click', function() {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -31,10 +44,11 @@ var getCurrentRoomList = function() {
|
||||
// solution but that is out of scope of this fiddle.
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=0; i<data.length; ++i) {
|
||||
addRoom(data[i]);
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
addRoom(rooms[i]);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
@@ -44,7 +58,7 @@ var getCurrentRoomList = function() {
|
||||
$('.createRoom').live('click', function() {
|
||||
var data = {};
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
@@ -78,54 +92,42 @@ $('.changeMembership').live('click', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$user", encodeURIComponent(member));
|
||||
url = url.replace("$membership", membership);
|
||||
|
||||
if (membership === "leave") {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "DELETE",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
var data = {
|
||||
membership: membership
|
||||
var data = {};
|
||||
|
||||
if (membership === "invite") {
|
||||
data = {
|
||||
user_id: member
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "PUT",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.joinAlias').live('click', function() {
|
||||
var roomAlias = $("#roomAlias").val();
|
||||
var url = "http://localhost:8080/matrix/client/api/v1/join/$roomalias?access_token=$token";
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "PUT",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({}),
|
||||
dataType: "json",
|
||||
|
||||
280
pylint.cfg
Normal file
280
pylint.cfg
Normal file
@@ -0,0 +1,280 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Profiled execution.
|
||||
profile=no
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time. See also the "--disable" option for examples.
|
||||
#enable=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=missing-docstring
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]".
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Add a comment according to your evaluation note. This is used by the global
|
||||
# evaluation report (RP0004).
|
||||
comment=no
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set).
|
||||
ignored-classes=SQLObject
|
||||
|
||||
# When zope mode is activated, add a predefined set of Zope acquired attributes
|
||||
# to generated-members.
|
||||
zope=no
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=REQUEST,acl_users,aq_parent
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the beginning of the name of dummy variables
|
||||
# (i.e. not used).
|
||||
dummy-variables-rgx=_$|dummy
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Required attributes for module, separated by a comma
|
||||
required-attributes=
|
||||
|
||||
# List of builtins function names that should not be used, separated by a comma
|
||||
bad-functions=map,filter,apply,input
|
||||
|
||||
# Regular expression which should only match correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression which should only match correct module level names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression which should only match correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression which should only match correct function names
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct method names
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct instance attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct argument names
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct variable names
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct attribute names in class
|
||||
# bodies
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression which should only match correct list comprehension /
|
||||
# generator expression variable names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,Run,_
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=__.*__
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=80
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=regsub,TERMIOS,Bastion,rexec
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of interface methods to ignore, separated by a comma. This is used for
|
||||
# instance to not check methods defines in Zope's Interface base class.
|
||||
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception
|
||||
47
scripts/check_event_hash.py
Normal file
47
scripts/check_event_hash.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from synapse.crypto.event_signing import *
|
||||
from syutil.base64util import encode_base64
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
class dictobj(dict):
|
||||
def __init__(self, *args, **kargs):
|
||||
dict.__init__(self, *args, **kargs)
|
||||
self.__dict__ = self
|
||||
|
||||
def get_dict(self):
|
||||
return dict(self)
|
||||
|
||||
def get_full_dict(self):
|
||||
return dict(self)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input_json", nargs="?", type=argparse.FileType('r'),
|
||||
default=sys.stdin)
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig()
|
||||
|
||||
event_json = dictobj(json.load(args.input_json))
|
||||
|
||||
algorithms = {
|
||||
"sha256": hashlib.sha256,
|
||||
}
|
||||
|
||||
for alg_name in event_json.hashes:
|
||||
if check_event_content_hash(event_json, algorithms[alg_name]):
|
||||
print "PASS content hash %s" % (alg_name,)
|
||||
else:
|
||||
print "FAIL content hash %s" % (alg_name,)
|
||||
|
||||
for algorithm in algorithms.values():
|
||||
name, h_bytes = compute_event_reference_hash(event_json, algorithm)
|
||||
print "Reference hash %s: %s" % (name, encode_base64(h_bytes))
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
|
||||
73
scripts/check_signature.py
Normal file
73
scripts/check_signature.py
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
from syutil.crypto.jsonsign import verify_signed_json
|
||||
from syutil.crypto.signing_key import (
|
||||
decode_verify_key_bytes, write_signing_keys
|
||||
)
|
||||
from syutil.base64util import decode_base64
|
||||
|
||||
import urllib2
|
||||
import json
|
||||
import sys
|
||||
import dns.resolver
|
||||
import pprint
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
def get_targets(server_name):
|
||||
if ":" in server_name:
|
||||
target, port = server_name.split(":")
|
||||
yield (target, int(port))
|
||||
return
|
||||
try:
|
||||
answers = dns.resolver.query("_matrix._tcp." + server_name, "SRV")
|
||||
for srv in answers:
|
||||
yield (srv.target, srv.port)
|
||||
except dns.resolver.NXDOMAIN:
|
||||
yield (server_name, 8448)
|
||||
|
||||
def get_server_keys(server_name, target, port):
|
||||
url = "https://%s:%i/_matrix/key/v1" % (target, port)
|
||||
keys = json.load(urllib2.urlopen(url))
|
||||
verify_keys = {}
|
||||
for key_id, key_base64 in keys["verify_keys"].items():
|
||||
verify_key = decode_verify_key_bytes(key_id, decode_base64(key_base64))
|
||||
verify_signed_json(keys, server_name, verify_key)
|
||||
verify_keys[key_id] = verify_key
|
||||
return verify_keys
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("signature_name")
|
||||
parser.add_argument("input_json", nargs="?", type=argparse.FileType('r'),
|
||||
default=sys.stdin)
|
||||
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig()
|
||||
|
||||
server_name = args.signature_name
|
||||
keys = {}
|
||||
for target, port in get_targets(server_name):
|
||||
try:
|
||||
keys = get_server_keys(server_name, target, port)
|
||||
print "Using keys from https://%s:%s/_matrix/key/v1" % (target, port)
|
||||
write_signing_keys(sys.stdout, keys.values())
|
||||
break
|
||||
except:
|
||||
logging.exception("Error talking to %s:%s", target, port)
|
||||
|
||||
json_to_check = json.load(args.input_json)
|
||||
print "Checking JSON:"
|
||||
for key_id in json_to_check["signatures"][args.signature_name]:
|
||||
try:
|
||||
key = keys[key_id]
|
||||
verify_signed_json(json_to_check, args.signature_name, key)
|
||||
print "PASS %s" % (key_id,)
|
||||
except:
|
||||
logging.exception("Check for key %s failed" % (key_id,))
|
||||
print "FAIL %s" % (key_id,)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/perl -pi
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
$copyright = <<EOT;
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
69
scripts/hash_history.py
Normal file
69
scripts/hash_history.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from synapse.storage.pdu import PduStore
|
||||
from synapse.storage.signatures import SignatureStore
|
||||
from synapse.storage._base import SQLBaseStore
|
||||
from synapse.federation.units import Pdu
|
||||
from synapse.crypto.event_signing import (
|
||||
add_event_pdu_content_hash, compute_pdu_event_reference_hash
|
||||
)
|
||||
from synapse.api.events.utils import prune_pdu
|
||||
from syutil.base64util import encode_base64, decode_base64
|
||||
from syutil.jsonutil import encode_canonical_json
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
class Store(object):
|
||||
_get_pdu_tuples = PduStore.__dict__["_get_pdu_tuples"]
|
||||
_get_pdu_content_hashes_txn = SignatureStore.__dict__["_get_pdu_content_hashes_txn"]
|
||||
_get_prev_pdu_hashes_txn = SignatureStore.__dict__["_get_prev_pdu_hashes_txn"]
|
||||
_get_pdu_origin_signatures_txn = SignatureStore.__dict__["_get_pdu_origin_signatures_txn"]
|
||||
_store_pdu_content_hash_txn = SignatureStore.__dict__["_store_pdu_content_hash_txn"]
|
||||
_store_pdu_reference_hash_txn = SignatureStore.__dict__["_store_pdu_reference_hash_txn"]
|
||||
_store_prev_pdu_hash_txn = SignatureStore.__dict__["_store_prev_pdu_hash_txn"]
|
||||
_simple_insert_txn = SQLBaseStore.__dict__["_simple_insert_txn"]
|
||||
|
||||
|
||||
store = Store()
|
||||
|
||||
|
||||
def select_pdus(cursor):
|
||||
cursor.execute(
|
||||
"SELECT pdu_id, origin FROM pdus ORDER BY depth ASC"
|
||||
)
|
||||
|
||||
ids = cursor.fetchall()
|
||||
|
||||
pdu_tuples = store._get_pdu_tuples(cursor, ids)
|
||||
|
||||
pdus = [Pdu.from_pdu_tuple(p) for p in pdu_tuples]
|
||||
|
||||
reference_hashes = {}
|
||||
|
||||
for pdu in pdus:
|
||||
try:
|
||||
if pdu.prev_pdus:
|
||||
print "PROCESS", pdu.pdu_id, pdu.origin, pdu.prev_pdus
|
||||
for pdu_id, origin, hashes in pdu.prev_pdus:
|
||||
ref_alg, ref_hsh = reference_hashes[(pdu_id, origin)]
|
||||
hashes[ref_alg] = encode_base64(ref_hsh)
|
||||
store._store_prev_pdu_hash_txn(cursor, pdu.pdu_id, pdu.origin, pdu_id, origin, ref_alg, ref_hsh)
|
||||
print "SUCCESS", pdu.pdu_id, pdu.origin, pdu.prev_pdus
|
||||
pdu = add_event_pdu_content_hash(pdu)
|
||||
ref_alg, ref_hsh = compute_pdu_event_reference_hash(pdu)
|
||||
reference_hashes[(pdu.pdu_id, pdu.origin)] = (ref_alg, ref_hsh)
|
||||
store._store_pdu_reference_hash_txn(cursor, pdu.pdu_id, pdu.origin, ref_alg, ref_hsh)
|
||||
|
||||
for alg, hsh_base64 in pdu.hashes.items():
|
||||
print alg, hsh_base64
|
||||
store._store_pdu_content_hash_txn(cursor, pdu.pdu_id, pdu.origin, alg, decode_base64(hsh_base64))
|
||||
|
||||
except:
|
||||
print "FAILED_", pdu.pdu_id, pdu.origin, pdu.prev_pdus
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(sys.argv[1])
|
||||
cursor = conn.cursor()
|
||||
select_pdus(cursor)
|
||||
conn.commit()
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
22
setup.py
22
setup.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -26,31 +26,39 @@ def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
setup(
|
||||
name="SynapseHomeServer",
|
||||
version="0.0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
name="matrix-synapse",
|
||||
version=read("VERSION").strip(),
|
||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||
description="Reference Synapse Home Server",
|
||||
install_requires=[
|
||||
"syutil==0.0.1",
|
||||
"syutil==0.0.2",
|
||||
"matrix_angular_sdk==0.5.1",
|
||||
"Twisted>=14.0.0",
|
||||
"service_identity>=1.0.0",
|
||||
"pyopenssl>=0.14",
|
||||
"pyyaml",
|
||||
"pyasn1",
|
||||
"pynacl",
|
||||
"daemonize",
|
||||
"py-bcrypt",
|
||||
],
|
||||
dependency_links=[
|
||||
"git+ssh://git@github.com/matrix-org/syutil.git#egg=syutil-0.0.1",
|
||||
"https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2",
|
||||
"https://github.com/pyca/pynacl/tarball/d4d3175589b892f6ea7c22f466e0e223853516fa#egg=pynacl-0.3.0",
|
||||
"https://github.com/matrix-org/matrix-angular-sdk/tarball/v0.5.1/#egg=matrix_angular_sdk-0.5.1",
|
||||
],
|
||||
setup_requires=[
|
||||
"setuptools_trial",
|
||||
"setuptools>=1.0.0", # Needs setuptools that supports git+ssh. It's not obvious when support for this was introduced.
|
||||
"setuptools>=1.0.0", # Needs setuptools that supports git+ssh.
|
||||
# TODO: Do we need this now? we don't use git+ssh.
|
||||
"mock"
|
||||
],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
long_description=read("README.rst"),
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
synctl=synapse.app.synctl:main
|
||||
synapse-homeserver=synapse.app.homeserver:run
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (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 synapse home server.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.1"
|
||||
__version__ = "0.5.3c"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,9 +17,14 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import AuthError, StoreError, Codes
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
from synapse.api.constants import Membership, JoinRules
|
||||
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
|
||||
from synapse.api.events.room import (
|
||||
RoomMemberEvent, RoomPowerLevelsEvent, RoomRedactionEvent,
|
||||
RoomJoinRulesEvent, RoomCreateEvent, RoomAliasesEvent,
|
||||
)
|
||||
from synapse.util.logutils import log_function
|
||||
from syutil.base64util import encode_base64
|
||||
|
||||
import logging
|
||||
|
||||
@@ -31,50 +36,96 @@ class Auth(object):
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastore()
|
||||
self.state = hs.get_state_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check(self, event, snapshot, raises=False):
|
||||
def check(self, event, auth_events):
|
||||
""" Checks if this event is correctly authed.
|
||||
|
||||
Returns:
|
||||
True if the auth checks pass.
|
||||
Raises:
|
||||
AuthError if there was a problem authorising this event. This will
|
||||
be raised only if raises=True.
|
||||
"""
|
||||
try:
|
||||
if hasattr(event, "room_id"):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
defer.returnValue(allowed)
|
||||
if not hasattr(event, "room_id"):
|
||||
raise AuthError(500, "Event has no room_id: %s" % event)
|
||||
if auth_events is None:
|
||||
# Oh, we don't know what the state of the room was, so we
|
||||
# are trusting that this is allowed (at least for now)
|
||||
logger.warn("Trusting event: %s", event.event_id)
|
||||
return True
|
||||
|
||||
if event.type == RoomCreateEvent.TYPE:
|
||||
# FIXME
|
||||
return True
|
||||
|
||||
# FIXME: Temp hack
|
||||
if event.type == RoomAliasesEvent.TYPE:
|
||||
return True
|
||||
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
allowed = self.is_membership_change_allowed(
|
||||
event, auth_events
|
||||
)
|
||||
if allowed:
|
||||
logger.debug("Allowing! %s", event)
|
||||
else:
|
||||
self._check_joined_room(
|
||||
member=snapshot.membership_state,
|
||||
user_id=snapshot.user_id,
|
||||
room_id=snapshot.room_id,
|
||||
)
|
||||
defer.returnValue(True)
|
||||
else:
|
||||
raise AuthError(500, "Unknown event: %s" % event)
|
||||
logger.debug("Denying! %s", event)
|
||||
return allowed
|
||||
|
||||
self.check_event_sender_in_room(event, auth_events)
|
||||
self._can_send_event(event, auth_events)
|
||||
|
||||
if event.type == RoomPowerLevelsEvent.TYPE:
|
||||
self._check_power_levels(event, auth_events)
|
||||
|
||||
if event.type == RoomRedactionEvent.TYPE:
|
||||
self._check_redaction(event, auth_events)
|
||||
|
||||
logger.debug("Allowing! %s", event)
|
||||
except AuthError as e:
|
||||
logger.info("Event auth check failed on event %s with msg: %s",
|
||||
event, e.msg)
|
||||
if raises:
|
||||
raise e
|
||||
defer.returnValue(False)
|
||||
logger.info(
|
||||
"Event auth check failed on event %s with msg: %s",
|
||||
event, e.msg
|
||||
)
|
||||
logger.info("Denying! %s", event)
|
||||
raise
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_joined_room(self, room_id, user_id):
|
||||
try:
|
||||
member = yield self.store.get_room_member(
|
||||
room_id=room_id,
|
||||
user_id=user_id
|
||||
)
|
||||
self._check_joined_room(member, user_id, room_id)
|
||||
defer.returnValue(member)
|
||||
except AttributeError:
|
||||
pass
|
||||
defer.returnValue(None)
|
||||
member = yield self.state.get_current_state(
|
||||
room_id=room_id,
|
||||
event_type=RoomMemberEvent.TYPE,
|
||||
state_key=user_id
|
||||
)
|
||||
self._check_joined_room(member, user_id, room_id)
|
||||
defer.returnValue(member)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_host_in_room(self, room_id, host):
|
||||
curr_state = yield self.state.get_current_state(room_id)
|
||||
|
||||
for event in curr_state:
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
try:
|
||||
if self.hs.parse_userid(event.state_key).domain != host:
|
||||
continue
|
||||
except:
|
||||
logger.warn("state_key not user_id: %s", event.state_key)
|
||||
continue
|
||||
|
||||
if event.content["membership"] == Membership.JOIN:
|
||||
defer.returnValue(True)
|
||||
|
||||
defer.returnValue(False)
|
||||
|
||||
def check_event_sender_in_room(self, event, auth_events):
|
||||
key = (RoomMemberEvent.TYPE, event.user_id, )
|
||||
member_event = auth_events.get(key)
|
||||
|
||||
return self._check_joined_room(
|
||||
member_event,
|
||||
event.user_id,
|
||||
event.room_id
|
||||
)
|
||||
|
||||
def _check_joined_room(self, member, user_id, room_id):
|
||||
if not member or member.membership != Membership.JOIN:
|
||||
@@ -82,39 +133,79 @@ class Auth(object):
|
||||
user_id, room_id, repr(member)
|
||||
))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_membership_change_allowed(self, event):
|
||||
target_user_id = event.state_key
|
||||
|
||||
# does this room even exist
|
||||
room = yield self.store.get_room(event.room_id)
|
||||
if not room:
|
||||
raise AuthError(403, "Room does not exist")
|
||||
|
||||
# get info about the caller
|
||||
try:
|
||||
caller = yield self.store.get_room_member(
|
||||
user_id=event.user_id,
|
||||
room_id=event.room_id)
|
||||
except:
|
||||
caller = None
|
||||
caller_in_room = caller and caller.membership == "join"
|
||||
|
||||
# get info about the target
|
||||
try:
|
||||
target = yield self.store.get_room_member(
|
||||
user_id=target_user_id,
|
||||
room_id=event.room_id)
|
||||
except:
|
||||
target = None
|
||||
target_in_room = target and target.membership == "join"
|
||||
|
||||
@log_function
|
||||
def is_membership_change_allowed(self, event, auth_events):
|
||||
membership = event.content["membership"]
|
||||
|
||||
# Check if this is the room creator joining:
|
||||
if len(event.prev_events) == 1 and Membership.JOIN == membership:
|
||||
# Get room creation event:
|
||||
key = (RoomCreateEvent.TYPE, "", )
|
||||
create = auth_events.get(key)
|
||||
if create and event.prev_events[0][0] == create.event_id:
|
||||
if create.content["creator"] == event.state_key:
|
||||
return True
|
||||
|
||||
target_user_id = event.state_key
|
||||
|
||||
# get info about the caller
|
||||
key = (RoomMemberEvent.TYPE, event.user_id, )
|
||||
caller = auth_events.get(key)
|
||||
|
||||
caller_in_room = caller and caller.membership == Membership.JOIN
|
||||
caller_invited = caller and caller.membership == Membership.INVITE
|
||||
|
||||
# get info about the target
|
||||
key = (RoomMemberEvent.TYPE, target_user_id, )
|
||||
target = auth_events.get(key)
|
||||
|
||||
target_in_room = target and target.membership == Membership.JOIN
|
||||
|
||||
key = (RoomJoinRulesEvent.TYPE, "", )
|
||||
join_rule_event = auth_events.get(key)
|
||||
if join_rule_event:
|
||||
join_rule = join_rule_event.content.get(
|
||||
"join_rule", JoinRules.INVITE
|
||||
)
|
||||
else:
|
||||
join_rule = JoinRules.INVITE
|
||||
|
||||
user_level = self._get_power_level_from_event_state(
|
||||
event,
|
||||
event.user_id,
|
||||
auth_events,
|
||||
)
|
||||
|
||||
ban_level, kick_level, redact_level = (
|
||||
self._get_ops_level_from_event_state(
|
||||
event,
|
||||
auth_events,
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"is_membership_change_allowed: %s",
|
||||
{
|
||||
"caller_in_room": caller_in_room,
|
||||
"caller_invited": caller_invited,
|
||||
"target_in_room": target_in_room,
|
||||
"membership": membership,
|
||||
"join_rule": join_rule,
|
||||
"target_user_id": target_user_id,
|
||||
"event.user_id": event.user_id,
|
||||
}
|
||||
)
|
||||
|
||||
if Membership.INVITE == membership:
|
||||
# TODO (erikj): We should probably handle this more intelligently
|
||||
# PRIVATE join rules.
|
||||
|
||||
# Invites are valid iff caller is in the room and target isn't.
|
||||
if not caller_in_room: # caller isn't joined
|
||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
raise AuthError(
|
||||
403,
|
||||
"%s not in room %s." % (event.user_id, event.room_id,)
|
||||
)
|
||||
elif target_in_room: # the target is already in the room.
|
||||
raise AuthError(403, "%s is already in the room." %
|
||||
target_user_id)
|
||||
@@ -124,23 +215,76 @@ class Auth(object):
|
||||
# joined: It's a NOOP
|
||||
if event.user_id != target_user_id:
|
||||
raise AuthError(403, "Cannot force another user to join.")
|
||||
elif room.is_public:
|
||||
pass # anyone can join public rooms.
|
||||
elif (not caller or caller.membership not in
|
||||
[Membership.INVITE, Membership.JOIN]):
|
||||
raise AuthError(403, "You are not invited to this room.")
|
||||
elif join_rule == JoinRules.PUBLIC:
|
||||
pass
|
||||
elif join_rule == JoinRules.INVITE:
|
||||
if not caller_in_room and not caller_invited:
|
||||
raise AuthError(403, "You are not invited to this room.")
|
||||
else:
|
||||
# TODO (erikj): may_join list
|
||||
# TODO (erikj): private rooms
|
||||
raise AuthError(403, "You are not allowed to join this room")
|
||||
elif Membership.LEAVE == membership:
|
||||
# TODO (erikj): Implement kicks.
|
||||
|
||||
if not caller_in_room: # trying to leave a room you aren't joined
|
||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
raise AuthError(
|
||||
403,
|
||||
"%s not in room %s." % (target_user_id, event.room_id,)
|
||||
)
|
||||
elif target_user_id != event.user_id:
|
||||
# trying to force another user to leave
|
||||
raise AuthError(403, "Cannot force %s to leave." %
|
||||
target_user_id)
|
||||
if kick_level:
|
||||
kick_level = int(kick_level)
|
||||
else:
|
||||
kick_level = 50 # FIXME (erikj): What should we do here?
|
||||
|
||||
if user_level < kick_level:
|
||||
raise AuthError(
|
||||
403, "You cannot kick user %s." % target_user_id
|
||||
)
|
||||
elif Membership.BAN == membership:
|
||||
if ban_level:
|
||||
ban_level = int(ban_level)
|
||||
else:
|
||||
ban_level = 50 # FIXME (erikj): What should we do here?
|
||||
|
||||
if user_level < ban_level:
|
||||
raise AuthError(403, "You don't have permission to ban")
|
||||
else:
|
||||
raise AuthError(500, "Unknown membership %s" % membership)
|
||||
|
||||
defer.returnValue(True)
|
||||
return True
|
||||
|
||||
def _get_power_level_from_event_state(self, event, user_id, auth_events):
|
||||
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||
power_level_event = auth_events.get(key)
|
||||
level = None
|
||||
if power_level_event:
|
||||
level = power_level_event.content.get("users", {}).get(user_id)
|
||||
if not level:
|
||||
level = power_level_event.content.get("users_default", 0)
|
||||
else:
|
||||
key = (RoomCreateEvent.TYPE, "", )
|
||||
create_event = auth_events.get(key)
|
||||
if (create_event is not None and
|
||||
create_event.content["creator"] == user_id):
|
||||
return 100
|
||||
|
||||
return level
|
||||
|
||||
def _get_ops_level_from_event_state(self, event, auth_events):
|
||||
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||
power_level_event = auth_events.get(key)
|
||||
|
||||
if power_level_event:
|
||||
return (
|
||||
power_level_event.content.get("ban", 50),
|
||||
power_level_event.content.get("kick", 50),
|
||||
power_level_event.content.get("redact", 50),
|
||||
)
|
||||
return None, None, None,
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_by_req(self, request):
|
||||
""" Get a registered user's ID.
|
||||
|
||||
@@ -153,7 +297,25 @@ class Auth(object):
|
||||
"""
|
||||
# Can optionally look elsewhere in the request (e.g. headers)
|
||||
try:
|
||||
return self.get_user_by_token(request.args["access_token"][0])
|
||||
access_token = request.args["access_token"][0]
|
||||
user_info = yield self.get_user_by_token(access_token)
|
||||
user = user_info["user"]
|
||||
|
||||
ip_addr = self.hs.get_ip_from_request(request)
|
||||
user_agent = request.requestHeaders.getRawHeaders(
|
||||
"User-Agent",
|
||||
default=[""]
|
||||
)[0]
|
||||
if user and access_token and ip_addr:
|
||||
yield self.store.insert_client_ip(
|
||||
user=user,
|
||||
access_token=access_token,
|
||||
device_id=user_info["device_id"],
|
||||
ip=ip_addr,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
defer.returnValue(user)
|
||||
except KeyError:
|
||||
raise AuthError(403, "Missing access token.")
|
||||
|
||||
@@ -162,17 +324,246 @@ class Auth(object):
|
||||
""" Get a registered user's ID.
|
||||
|
||||
Args:
|
||||
token (str)- The access token to get the user by.
|
||||
token (str): The access token to get the user by.
|
||||
Returns:
|
||||
UserID : User ID object of the user who has that access token.
|
||||
dict : dict that includes the user, device_id, and whether the
|
||||
user is a server admin.
|
||||
Raises:
|
||||
AuthError if no user by that token exists or the token is invalid.
|
||||
"""
|
||||
try:
|
||||
user_id = yield self.store.get_user_by_token(token=token)
|
||||
if not user_id:
|
||||
ret = yield self.store.get_user_by_token(token=token)
|
||||
if not ret:
|
||||
raise StoreError()
|
||||
defer.returnValue(self.hs.parse_userid(user_id))
|
||||
|
||||
user_info = {
|
||||
"admin": bool(ret.get("admin", False)),
|
||||
"device_id": ret.get("device_id"),
|
||||
"user": self.hs.parse_userid(ret.get("name")),
|
||||
}
|
||||
|
||||
defer.returnValue(user_info)
|
||||
except StoreError:
|
||||
raise AuthError(403, "Unrecognised access token.",
|
||||
errcode=Codes.UNKNOWN_TOKEN)
|
||||
|
||||
def is_server_admin(self, user):
|
||||
return self.store.is_server_admin(user)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def add_auth_events(self, event):
|
||||
if event.type == RoomCreateEvent.TYPE:
|
||||
event.auth_events = []
|
||||
return
|
||||
|
||||
auth_events = []
|
||||
|
||||
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||
power_level_event = event.old_state_events.get(key)
|
||||
|
||||
if power_level_event:
|
||||
auth_events.append(power_level_event.event_id)
|
||||
|
||||
key = (RoomJoinRulesEvent.TYPE, "", )
|
||||
join_rule_event = event.old_state_events.get(key)
|
||||
|
||||
key = (RoomMemberEvent.TYPE, event.user_id, )
|
||||
member_event = event.old_state_events.get(key)
|
||||
|
||||
key = (RoomCreateEvent.TYPE, "", )
|
||||
create_event = event.old_state_events.get(key)
|
||||
if create_event:
|
||||
auth_events.append(create_event.event_id)
|
||||
|
||||
if join_rule_event:
|
||||
join_rule = join_rule_event.content.get("join_rule")
|
||||
is_public = join_rule == JoinRules.PUBLIC if join_rule else False
|
||||
else:
|
||||
is_public = False
|
||||
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
e_type = event.content["membership"]
|
||||
if e_type in [Membership.JOIN, Membership.INVITE]:
|
||||
if join_rule_event:
|
||||
auth_events.append(join_rule_event.event_id)
|
||||
|
||||
if member_event and not is_public:
|
||||
auth_events.append(member_event.event_id)
|
||||
elif member_event:
|
||||
if member_event.content["membership"] == Membership.JOIN:
|
||||
auth_events.append(member_event.event_id)
|
||||
|
||||
hashes = yield self.store.get_event_reference_hashes(
|
||||
auth_events
|
||||
)
|
||||
hashes = [
|
||||
{
|
||||
k: encode_base64(v) for k, v in h.items()
|
||||
if k == "sha256"
|
||||
}
|
||||
for h in hashes
|
||||
]
|
||||
event.auth_events = zip(auth_events, hashes)
|
||||
|
||||
@log_function
|
||||
def _can_send_event(self, event, auth_events):
|
||||
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||
send_level_event = auth_events.get(key)
|
||||
send_level = None
|
||||
if send_level_event:
|
||||
send_level = send_level_event.content.get("events", {}).get(
|
||||
event.type
|
||||
)
|
||||
if not send_level:
|
||||
if hasattr(event, "state_key"):
|
||||
send_level = send_level_event.content.get(
|
||||
"state_default", 50
|
||||
)
|
||||
else:
|
||||
send_level = send_level_event.content.get(
|
||||
"events_default", 0
|
||||
)
|
||||
|
||||
if send_level:
|
||||
send_level = int(send_level)
|
||||
else:
|
||||
send_level = 0
|
||||
|
||||
user_level = self._get_power_level_from_event_state(
|
||||
event,
|
||||
event.user_id,
|
||||
auth_events,
|
||||
)
|
||||
|
||||
if user_level:
|
||||
user_level = int(user_level)
|
||||
else:
|
||||
user_level = 0
|
||||
|
||||
if user_level < send_level:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You don't have permission to post that to the room. " +
|
||||
"user_level (%d) < send_level (%d)" % (user_level, send_level)
|
||||
)
|
||||
|
||||
# Check state_key
|
||||
if hasattr(event, "state_key"):
|
||||
if not event.state_key.startswith("_"):
|
||||
if event.state_key.startswith("@"):
|
||||
if event.state_key != event.user_id:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You are not allowed to set others state"
|
||||
)
|
||||
else:
|
||||
sender_domain = self.hs.parse_userid(
|
||||
event.user_id
|
||||
).domain
|
||||
|
||||
if sender_domain != event.state_key:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You are not allowed to set others state"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def _check_redaction(self, event, auth_events):
|
||||
user_level = self._get_power_level_from_event_state(
|
||||
event,
|
||||
event.user_id,
|
||||
auth_events,
|
||||
)
|
||||
|
||||
_, _, redact_level = self._get_ops_level_from_event_state(
|
||||
event,
|
||||
auth_events,
|
||||
)
|
||||
|
||||
if user_level < redact_level:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You don't have permission to redact events"
|
||||
)
|
||||
|
||||
def _check_power_levels(self, event, auth_events):
|
||||
user_list = event.content.get("users", {})
|
||||
# Validate users
|
||||
for k, v in user_list.items():
|
||||
try:
|
||||
self.hs.parse_userid(k)
|
||||
except:
|
||||
raise SynapseError(400, "Not a valid user_id: %s" % (k,))
|
||||
|
||||
try:
|
||||
int(v)
|
||||
except:
|
||||
raise SynapseError(400, "Not a valid power level: %s" % (v,))
|
||||
|
||||
key = (event.type, event.state_key, )
|
||||
current_state = auth_events.get(key)
|
||||
|
||||
if not current_state:
|
||||
return
|
||||
|
||||
user_level = self._get_power_level_from_event_state(
|
||||
event,
|
||||
event.user_id,
|
||||
auth_events,
|
||||
)
|
||||
|
||||
# Check other levels:
|
||||
levels_to_check = [
|
||||
("users_default", []),
|
||||
("events_default", []),
|
||||
("ban", []),
|
||||
("redact", []),
|
||||
("kick", []),
|
||||
]
|
||||
|
||||
old_list = current_state.content.get("users")
|
||||
for user in set(old_list.keys() + user_list.keys()):
|
||||
levels_to_check.append(
|
||||
(user, ["users"])
|
||||
)
|
||||
|
||||
old_list = current_state.content.get("events")
|
||||
new_list = event.content.get("events")
|
||||
for ev_id in set(old_list.keys() + new_list.keys()):
|
||||
levels_to_check.append(
|
||||
(ev_id, ["events"])
|
||||
)
|
||||
|
||||
old_state = current_state.content
|
||||
new_state = event.content
|
||||
|
||||
for level_to_check, dir in levels_to_check:
|
||||
old_loc = old_state
|
||||
for d in dir:
|
||||
old_loc = old_loc.get(d, {})
|
||||
|
||||
new_loc = new_state
|
||||
for d in dir:
|
||||
new_loc = new_loc.get(d, {})
|
||||
|
||||
if level_to_check in old_loc:
|
||||
old_level = int(old_loc[level_to_check])
|
||||
else:
|
||||
old_level = None
|
||||
|
||||
if level_to_check in new_loc:
|
||||
new_level = int(new_loc[level_to_check])
|
||||
else:
|
||||
new_level = None
|
||||
|
||||
if new_level is not None and old_level is not None:
|
||||
if new_level == old_level:
|
||||
continue
|
||||
|
||||
if old_level > user_level or new_level > user_level:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You don't have permission to add ops level greater "
|
||||
"than your own"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,8 @@ class Membership(object):
|
||||
JOIN = u"join"
|
||||
KNOCK = u"knock"
|
||||
LEAVE = u"leave"
|
||||
LIST = (INVITE, JOIN, KNOCK, LEAVE)
|
||||
BAN = u"ban"
|
||||
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
|
||||
|
||||
|
||||
class Feedback(object):
|
||||
@@ -42,3 +43,19 @@ class PresenceState(object):
|
||||
UNAVAILABLE = u"unavailable"
|
||||
ONLINE = u"online"
|
||||
FREE_FOR_CHAT = u"free_for_chat"
|
||||
|
||||
|
||||
class JoinRules(object):
|
||||
PUBLIC = u"public"
|
||||
KNOCK = u"knock"
|
||||
INVITE = u"invite"
|
||||
PRIVATE = u"private"
|
||||
|
||||
|
||||
class LoginType(object):
|
||||
PASSWORD = u"m.login.password"
|
||||
OAUTH = u"m.login.oauth2"
|
||||
EMAIL_CODE = u"m.login.email.code"
|
||||
EMAIL_URL = u"m.login.email.url"
|
||||
EMAIL_IDENTITY = u"m.login.email.identity"
|
||||
RECAPTCHA = u"m.login.recaptcha"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,8 +17,11 @@
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Codes(object):
|
||||
UNAUTHORIZED = "M_UNAUTHORIZED"
|
||||
FORBIDDEN = "M_FORBIDDEN"
|
||||
BAD_JSON = "M_BAD_JSON"
|
||||
NOT_JSON = "M_NOT_JSON"
|
||||
@@ -28,31 +31,44 @@ class Codes(object):
|
||||
UNKNOWN = "M_UNKNOWN"
|
||||
NOT_FOUND = "M_NOT_FOUND"
|
||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||
|
||||
|
||||
class CodeMessageException(Exception):
|
||||
"""An exception with integer code and message string attributes."""
|
||||
|
||||
def __init__(self, code, msg):
|
||||
logging.error("%s: %s, %s", type(self).__name__, code, msg)
|
||||
logger.info("%s: %s, %s", type(self).__name__, code, msg)
|
||||
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
self.response_code_message = None
|
||||
|
||||
def error_dict(self):
|
||||
return cs_error(self.msg)
|
||||
|
||||
|
||||
class SynapseError(CodeMessageException):
|
||||
"""A base error which can be caught for all synapse events."""
|
||||
def __init__(self, code, msg, errcode=""):
|
||||
def __init__(self, code, msg, errcode=Codes.UNKNOWN):
|
||||
"""Constructs a synapse error.
|
||||
|
||||
Args:
|
||||
code (int): The integer error code (typically an HTTP response code)
|
||||
code (int): The integer error code (an HTTP response code)
|
||||
msg (str): The human-readable error message.
|
||||
err (str): The error code e.g 'M_FORBIDDEN'
|
||||
"""
|
||||
super(SynapseError, self).__init__(code, msg)
|
||||
self.errcode = errcode
|
||||
|
||||
def error_dict(self):
|
||||
return cs_error(
|
||||
self.msg,
|
||||
self.errcode,
|
||||
)
|
||||
|
||||
|
||||
class RoomError(SynapseError):
|
||||
"""An error raised when a room event fails."""
|
||||
@@ -91,15 +107,43 @@ class StoreError(SynapseError):
|
||||
pass
|
||||
|
||||
|
||||
def cs_exception(exception):
|
||||
if isinstance(exception, SynapseError):
|
||||
class InvalidCaptchaError(SynapseError):
|
||||
def __init__(self, code=400, msg="Invalid captcha.", error_url=None,
|
||||
errcode=Codes.CAPTCHA_INVALID):
|
||||
super(InvalidCaptchaError, self).__init__(code, msg, errcode)
|
||||
self.error_url = error_url
|
||||
|
||||
def error_dict(self):
|
||||
return cs_error(
|
||||
exception.msg,
|
||||
Codes.UNKNOWN if not exception.errcode else exception.errcode)
|
||||
elif isinstance(exception, CodeMessageException):
|
||||
return cs_error(exception.msg)
|
||||
self.msg,
|
||||
self.errcode,
|
||||
error_url=self.error_url,
|
||||
)
|
||||
|
||||
|
||||
class LimitExceededError(SynapseError):
|
||||
"""A client has sent too many requests and is being throttled.
|
||||
"""
|
||||
def __init__(self, code=429, msg="Too Many Requests", retry_after_ms=None,
|
||||
errcode=Codes.LIMIT_EXCEEDED):
|
||||
super(LimitExceededError, self).__init__(code, msg, errcode)
|
||||
self.retry_after_ms = retry_after_ms
|
||||
self.response_code_message = "Too Many Requests"
|
||||
|
||||
def error_dict(self):
|
||||
return cs_error(
|
||||
self.msg,
|
||||
self.errcode,
|
||||
retry_after_ms=self.retry_after_ms,
|
||||
)
|
||||
|
||||
|
||||
def cs_exception(exception):
|
||||
if isinstance(exception, CodeMessageException):
|
||||
return exception.error_dict()
|
||||
else:
|
||||
logging.error("Unknown exception type: %s", type(exception))
|
||||
logger.error("Unknown exception type: %s", type(exception))
|
||||
return {}
|
||||
|
||||
|
||||
def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
|
||||
@@ -117,3 +161,37 @@ def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
|
||||
for key, value in kwargs.iteritems():
|
||||
err[key] = value
|
||||
return err
|
||||
|
||||
|
||||
class FederationError(RuntimeError):
|
||||
""" This class is used to inform remote home servers about erroneous
|
||||
PDUs they sent us.
|
||||
|
||||
FATAL: The remote server could not interpret the source event.
|
||||
(e.g., it was missing a required field)
|
||||
ERROR: The remote server interpreted the event, but it failed some other
|
||||
check (e.g. auth)
|
||||
WARN: The remote server accepted the event, but believes some part of it
|
||||
is wrong (e.g., it referred to an invalid event)
|
||||
"""
|
||||
|
||||
def __init__(self, level, code, reason, affected, source=None):
|
||||
if level not in ["FATAL", "ERROR", "WARN"]:
|
||||
raise ValueError("Level is not valid: %s" % (level,))
|
||||
self.level = level
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
self.affected = affected
|
||||
self.source = source
|
||||
|
||||
msg = "%s %s: %s" % (level, code, reason,)
|
||||
super(FederationError, self).__init__(msg)
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"level": self.level,
|
||||
"code": self.code,
|
||||
"reason": self.reason,
|
||||
"affected": self.affected,
|
||||
"source": self.source if self.source else self.affected,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -13,10 +13,23 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.errors import SynapseError, Codes
|
||||
from synapse.util.jsonobject import JsonEncodedObject
|
||||
|
||||
|
||||
def serialize_event(hs, e):
|
||||
# FIXME(erikj): To handle the case of presence events and the like
|
||||
if not isinstance(e, SynapseEvent):
|
||||
return e
|
||||
|
||||
# Should this strip out None's?
|
||||
d = {k: v for k, v in e.get_dict().items()}
|
||||
if "age_ts" in d:
|
||||
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
|
||||
del d["age_ts"]
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class SynapseEvent(JsonEncodedObject):
|
||||
|
||||
"""Base class for Synapse events. These are JSON objects which must abide
|
||||
@@ -42,16 +55,26 @@ class SynapseEvent(JsonEncodedObject):
|
||||
"user_id", # sender/initiator
|
||||
"content", # HTTP body, JSON
|
||||
"state_key",
|
||||
"age_ts",
|
||||
"prev_content",
|
||||
"replaces_state",
|
||||
"redacted_because",
|
||||
"origin_server_ts",
|
||||
]
|
||||
|
||||
internal_keys = [
|
||||
"is_state",
|
||||
"prev_events",
|
||||
"prev_state",
|
||||
"depth",
|
||||
"destinations",
|
||||
"origin",
|
||||
"outlier",
|
||||
"redacted",
|
||||
"prev_events",
|
||||
"hashes",
|
||||
"signatures",
|
||||
"prev_state",
|
||||
"auth_events",
|
||||
"state_hash",
|
||||
]
|
||||
|
||||
required_keys = [
|
||||
@@ -60,10 +83,12 @@ class SynapseEvent(JsonEncodedObject):
|
||||
"content",
|
||||
]
|
||||
|
||||
outlier = False
|
||||
|
||||
def __init__(self, raises=True, **kwargs):
|
||||
super(SynapseEvent, self).__init__(**kwargs)
|
||||
if "content" in kwargs:
|
||||
self.check_json(self.content, raises=raises)
|
||||
# if "content" in kwargs:
|
||||
# self.check_json(self.content, raises=raises)
|
||||
|
||||
def get_content_template(self):
|
||||
""" Retrieve the JSON template for this event as a dict.
|
||||
@@ -94,61 +119,30 @@ class SynapseEvent(JsonEncodedObject):
|
||||
"""
|
||||
raise NotImplementedError("get_content_template not implemented.")
|
||||
|
||||
def check_json(self, content, raises=True):
|
||||
"""Checks the given JSON content abides by the rules of the template.
|
||||
def get_pdu_json(self, time_now=None):
|
||||
pdu_json = self.get_full_dict()
|
||||
pdu_json.pop("destinations", None)
|
||||
pdu_json.pop("outlier", None)
|
||||
pdu_json.pop("replaces_state", None)
|
||||
pdu_json.pop("redacted", None)
|
||||
pdu_json.pop("prev_content", None)
|
||||
state_hash = pdu_json.pop("state_hash", None)
|
||||
if state_hash is not None:
|
||||
pdu_json.setdefault("unsigned", {})["state_hash"] = state_hash
|
||||
content = pdu_json.get("content", {})
|
||||
content.pop("prev", None)
|
||||
if time_now is not None and "age_ts" in pdu_json:
|
||||
age = time_now - pdu_json["age_ts"]
|
||||
pdu_json.setdefault("unsigned", {})["age"] = int(age)
|
||||
del pdu_json["age_ts"]
|
||||
user_id = pdu_json.pop("user_id")
|
||||
pdu_json["sender"] = user_id
|
||||
return pdu_json
|
||||
|
||||
Args:
|
||||
content : A JSON object to check.
|
||||
raises: True to raise a SynapseError if the check fails.
|
||||
Returns:
|
||||
True if the content passes the template. Returns False if the check
|
||||
fails and raises=False.
|
||||
Raises:
|
||||
SynapseError if the check fails and raises=True.
|
||||
"""
|
||||
# recursively call to inspect each layer
|
||||
err_msg = self._check_json(content, self.get_content_template())
|
||||
if err_msg:
|
||||
if raises:
|
||||
raise SynapseError(400, err_msg, Codes.BAD_JSON)
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _check_json(self, content, template):
|
||||
"""Check content and template matches.
|
||||
class SynapseStateEvent(SynapseEvent):
|
||||
|
||||
If the template is a dict, each key in the dict will be validated with
|
||||
the content, else it will just compare the types of content and
|
||||
template. This basic type check is required because this function will
|
||||
be recursively called and could be called with just strs or ints.
|
||||
|
||||
Args:
|
||||
content: The content to validate.
|
||||
template: The validation template.
|
||||
Returns:
|
||||
str: An error message if the validation fails, else None.
|
||||
"""
|
||||
if type(content) != type(template):
|
||||
return "Mismatched types: %s" % template
|
||||
|
||||
if type(template) == dict:
|
||||
for key in template:
|
||||
if key not in content:
|
||||
return "Missing %s key" % key
|
||||
|
||||
if type(content[key]) != type(template[key]):
|
||||
return "Key %s is of the wrong type." % key
|
||||
|
||||
if type(content[key]) == dict:
|
||||
# we must go deeper
|
||||
msg = self._check_json(content[key], template[key])
|
||||
if msg:
|
||||
return msg
|
||||
elif type(content[key]) == list:
|
||||
# make sure each item type in content matches the template
|
||||
for entry in content[key]:
|
||||
msg = self._check_json(entry, template[key][0])
|
||||
if msg:
|
||||
return msg
|
||||
def __init__(self, **kwargs):
|
||||
if "state_key" not in kwargs:
|
||||
kwargs["state_key"] = ""
|
||||
super(SynapseStateEvent, self).__init__(**kwargs)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -16,8 +16,13 @@
|
||||
from synapse.api.events.room import (
|
||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||
RoomPowerLevelsEvent, RoomJoinRulesEvent,
|
||||
RoomCreateEvent,
|
||||
RoomRedactionEvent,
|
||||
)
|
||||
|
||||
from synapse.types import EventID
|
||||
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
|
||||
@@ -30,7 +35,11 @@ class EventFactory(object):
|
||||
RoomMemberEvent,
|
||||
FeedbackEvent,
|
||||
InviteJoinEvent,
|
||||
RoomConfigEvent
|
||||
RoomConfigEvent,
|
||||
RoomPowerLevelsEvent,
|
||||
RoomJoinRulesEvent,
|
||||
RoomCreateEvent,
|
||||
RoomRedactionEvent,
|
||||
]
|
||||
|
||||
def __init__(self, hs):
|
||||
@@ -39,14 +48,39 @@ class EventFactory(object):
|
||||
self._event_list[event_class.TYPE] = event_class
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
self.hs = hs
|
||||
|
||||
self.event_id_count = 0
|
||||
|
||||
def create_event_id(self):
|
||||
i = str(self.event_id_count)
|
||||
self.event_id_count += 1
|
||||
|
||||
local_part = str(int(self.clock.time())) + i + random_string(5)
|
||||
|
||||
e_id = EventID.create_local(local_part, self.hs)
|
||||
|
||||
return e_id.to_string()
|
||||
|
||||
def create_event(self, etype=None, **kwargs):
|
||||
kwargs["type"] = etype
|
||||
if "event_id" not in kwargs:
|
||||
kwargs["event_id"] = random_string(10)
|
||||
kwargs["event_id"] = self.create_event_id()
|
||||
kwargs["origin"] = self.hs.hostname
|
||||
else:
|
||||
ev_id = self.hs.parse_eventid(kwargs["event_id"])
|
||||
kwargs["origin"] = ev_id.domain
|
||||
|
||||
if "ts" not in kwargs:
|
||||
kwargs["ts"] = int(self.clock.time_msec())
|
||||
if "origin_server_ts" not in kwargs:
|
||||
kwargs["origin_server_ts"] = int(self.clock.time_msec())
|
||||
|
||||
# The "age" key is a delta timestamp that should be converted into an
|
||||
# absolute timestamp the minute we see it.
|
||||
if "age" in kwargs:
|
||||
kwargs["age_ts"] = int(self.clock.time_msec()) - int(kwargs["age"])
|
||||
del kwargs["age"]
|
||||
elif "age_ts" not in kwargs:
|
||||
kwargs["age_ts"] = int(self.clock.time_msec())
|
||||
|
||||
if etype in self._event_list:
|
||||
handler = self._event_list[etype]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
from synapse.api.constants import Feedback, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from . import SynapseEvent
|
||||
from . import SynapseEvent, SynapseStateEvent
|
||||
|
||||
|
||||
class GenericEvent(SynapseEvent):
|
||||
@@ -103,8 +103,7 @@ class FeedbackEvent(SynapseEvent):
|
||||
def get_content_template(self):
|
||||
return {
|
||||
"type": u"string",
|
||||
"target_event_id": u"string",
|
||||
"msg_sender_id": u"string"
|
||||
"target_event_id": u"string"
|
||||
}
|
||||
|
||||
|
||||
@@ -132,3 +131,40 @@ class RoomConfigEvent(SynapseEvent):
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RoomCreateEvent(SynapseStateEvent):
|
||||
TYPE = "m.room.create"
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RoomJoinRulesEvent(SynapseStateEvent):
|
||||
TYPE = "m.room.join_rules"
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RoomPowerLevelsEvent(SynapseStateEvent):
|
||||
TYPE = "m.room.power_levels"
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RoomAliasesEvent(SynapseStateEvent):
|
||||
TYPE = "m.room.aliases"
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RoomRedactionEvent(SynapseEvent):
|
||||
TYPE = "m.room.redaction"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + ["redacts"]
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
85
synapse/api/events/utils.py
Normal file
85
synapse/api/events/utils.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 .room import (
|
||||
RoomMemberEvent, RoomJoinRulesEvent, RoomPowerLevelsEvent,
|
||||
RoomAliasesEvent, RoomCreateEvent,
|
||||
)
|
||||
|
||||
|
||||
def prune_event(event):
|
||||
""" Returns a pruned version of the given event, which removes all keys we
|
||||
don't know about or think could potentially be dodgy.
|
||||
|
||||
This is used when we "redact" an event. We want to remove all fields that
|
||||
the user has specified, but we do want to keep necessary information like
|
||||
type, state_key etc.
|
||||
"""
|
||||
event_type = event.type
|
||||
|
||||
allowed_keys = [
|
||||
"event_id",
|
||||
"user_id",
|
||||
"room_id",
|
||||
"hashes",
|
||||
"signatures",
|
||||
"content",
|
||||
"type",
|
||||
"state_key",
|
||||
"depth",
|
||||
"prev_events",
|
||||
"prev_state",
|
||||
"auth_events",
|
||||
"origin",
|
||||
"origin_server_ts",
|
||||
]
|
||||
|
||||
new_content = {}
|
||||
|
||||
def add_fields(*fields):
|
||||
for field in fields:
|
||||
if field in event.content:
|
||||
new_content[field] = event.content[field]
|
||||
|
||||
if event_type == RoomMemberEvent.TYPE:
|
||||
add_fields("membership")
|
||||
elif event_type == RoomCreateEvent.TYPE:
|
||||
add_fields("creator")
|
||||
elif event_type == RoomJoinRulesEvent.TYPE:
|
||||
add_fields("join_rule")
|
||||
elif event_type == RoomPowerLevelsEvent.TYPE:
|
||||
add_fields(
|
||||
"users",
|
||||
"users_default",
|
||||
"events",
|
||||
"events_default",
|
||||
"events_default",
|
||||
"state_default",
|
||||
"ban",
|
||||
"kick",
|
||||
"redact",
|
||||
)
|
||||
elif event_type == RoomAliasesEvent.TYPE:
|
||||
add_fields("aliases")
|
||||
|
||||
allowed_fields = {
|
||||
k: v
|
||||
for k, v in event.get_full_dict().items()
|
||||
if k in allowed_keys
|
||||
}
|
||||
|
||||
allowed_fields["content"] = new_content
|
||||
|
||||
return type(event)(**allowed_fields)
|
||||
87
synapse/api/events/validator.py
Normal file
87
synapse/api/events/validator.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 synapse.api.errors import SynapseError, Codes
|
||||
|
||||
|
||||
class EventValidator(object):
|
||||
def __init__(self, hs):
|
||||
pass
|
||||
|
||||
def validate(self, event):
|
||||
"""Checks the given JSON content abides by the rules of the template.
|
||||
|
||||
Args:
|
||||
content : A JSON object to check.
|
||||
raises: True to raise a SynapseError if the check fails.
|
||||
Returns:
|
||||
True if the content passes the template. Returns False if the check
|
||||
fails and raises=False.
|
||||
Raises:
|
||||
SynapseError if the check fails and raises=True.
|
||||
"""
|
||||
# recursively call to inspect each layer
|
||||
err_msg = self._check_json_template(
|
||||
event.content,
|
||||
event.get_content_template()
|
||||
)
|
||||
if err_msg:
|
||||
raise SynapseError(400, err_msg, Codes.BAD_JSON)
|
||||
else:
|
||||
return True
|
||||
|
||||
def _check_json_template(self, content, template):
|
||||
"""Check content and template matches.
|
||||
|
||||
If the template is a dict, each key in the dict will be validated with
|
||||
the content, else it will just compare the types of content and
|
||||
template. This basic type check is required because this function will
|
||||
be recursively called and could be called with just strs or ints.
|
||||
|
||||
Args:
|
||||
content: The content to validate.
|
||||
template: The validation template.
|
||||
Returns:
|
||||
str: An error message if the validation fails, else None.
|
||||
"""
|
||||
if type(content) != type(template):
|
||||
return "Mismatched types: %s" % template
|
||||
|
||||
if type(template) == dict:
|
||||
for key in template:
|
||||
if key not in content:
|
||||
return "Missing %s key" % key
|
||||
|
||||
if type(content[key]) != type(template[key]):
|
||||
return "Key %s is of the wrong type (got %s, want %s)" % (
|
||||
key, type(content[key]), type(template[key]))
|
||||
|
||||
if type(content[key]) == dict:
|
||||
# we must go deeper
|
||||
msg = self._check_json_template(
|
||||
content[key],
|
||||
template[key]
|
||||
)
|
||||
if msg:
|
||||
return msg
|
||||
elif type(content[key]) == list:
|
||||
# make sure each item type in content matches the template
|
||||
for entry in content[key]:
|
||||
msg = self._check_json_template(
|
||||
entry,
|
||||
template[key][0]
|
||||
)
|
||||
if msg:
|
||||
return msg
|
||||
79
synapse/api/ratelimiting.py
Normal file
79
synapse/api/ratelimiting.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Copyright 2014 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 collections
|
||||
|
||||
|
||||
class Ratelimiter(object):
|
||||
"""
|
||||
Ratelimit message sending by user.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.message_counts = collections.OrderedDict()
|
||||
|
||||
def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count):
|
||||
"""Can the user send a message?
|
||||
Args:
|
||||
user_id: The user sending a message.
|
||||
time_now_s: The time now.
|
||||
msg_rate_hz: The long term number of messages a user can send in a
|
||||
second.
|
||||
burst_count: How many messages the user can send before being
|
||||
limited.
|
||||
Returns:
|
||||
A pair of a bool indicating if they can send a message now and a
|
||||
time in seconds of when they can next send a message.
|
||||
"""
|
||||
self.prune_message_counts(time_now_s)
|
||||
message_count, time_start, _ignored = self.message_counts.pop(
|
||||
user_id, (0., time_now_s, None),
|
||||
)
|
||||
time_delta = time_now_s - time_start
|
||||
sent_count = message_count - time_delta * msg_rate_hz
|
||||
if sent_count < 0:
|
||||
allowed = True
|
||||
time_start = time_now_s
|
||||
message_count = 1.
|
||||
elif sent_count > burst_count - 1.:
|
||||
allowed = False
|
||||
else:
|
||||
allowed = True
|
||||
message_count += 1
|
||||
|
||||
self.message_counts[user_id] = (
|
||||
message_count, time_start, msg_rate_hz
|
||||
)
|
||||
|
||||
if msg_rate_hz > 0:
|
||||
time_allowed = (
|
||||
time_start + (message_count - burst_count + 1) / msg_rate_hz
|
||||
)
|
||||
if time_allowed < time_now_s:
|
||||
time_allowed = time_now_s
|
||||
else:
|
||||
time_allowed = -1
|
||||
|
||||
return allowed, time_allowed
|
||||
|
||||
def prune_message_counts(self, time_now_s):
|
||||
for user_id in self.message_counts.keys():
|
||||
message_count, time_start, msg_rate_hz = (
|
||||
self.message_counts[user_id]
|
||||
)
|
||||
time_delta = time_now_s - time_start
|
||||
if message_count - time_delta * msg_rate_hz > 0:
|
||||
break
|
||||
else:
|
||||
del self.message_counts[user_id]
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
"""Contains the URL paths to prefix various aspects of the server with. """
|
||||
|
||||
CLIENT_PREFIX = "/matrix/client/api/v1"
|
||||
FEDERATION_PREFIX = "/matrix/federation/v1"
|
||||
WEB_CLIENT_PREFIX = "/matrix/client"
|
||||
CONTENT_REPO_PREFIX = "/matrix/content"
|
||||
CLIENT_PREFIX = "/_matrix/client/api/v1"
|
||||
FEDERATION_PREFIX = "/_matrix/federation/v1"
|
||||
WEB_CLIENT_PREFIX = "/_matrix/client"
|
||||
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||
SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -14,55 +14,44 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.storage import read_schema
|
||||
from synapse.storage import prepare_database
|
||||
|
||||
from synapse.server import HomeServer
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.enterprise import adbapi
|
||||
from twisted.python.log import PythonLoggingObserver
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.static import File
|
||||
from twisted.web.server import Site
|
||||
from synapse.http.server import JsonResource, RootRedirect, ContentRepoResource
|
||||
from synapse.http.client import TwistedHttpClient
|
||||
from synapse.http.server import JsonResource, RootRedirect
|
||||
from synapse.http.content_repository import ContentRepoResource
|
||||
from synapse.http.server_key_resource import LocalKey
|
||||
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
||||
from synapse.api.urls import (
|
||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
|
||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
|
||||
SERVER_KEY_PREFIX,
|
||||
)
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.crypto import context_factory
|
||||
from synapse.util.logcontext import LoggingContext
|
||||
|
||||
from daemonize import Daemonize
|
||||
import twisted.manhole.telnet
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import sqlite3
|
||||
import syweb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SCHEMAS = [
|
||||
"transactions",
|
||||
"pdu",
|
||||
"users",
|
||||
"profiles",
|
||||
"presence",
|
||||
"im",
|
||||
"room_aliases",
|
||||
]
|
||||
|
||||
|
||||
# Remember to update this number every time an incompatible change is made to
|
||||
# database schema files, so the users will be informed on server restarts.
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
class SynapseHomeServer(HomeServer):
|
||||
|
||||
def build_http_client(self):
|
||||
return TwistedHttpClient()
|
||||
return MatrixFederationHttpClient(self)
|
||||
|
||||
def build_resource_for_client(self):
|
||||
return JsonResource()
|
||||
@@ -71,50 +60,25 @@ class SynapseHomeServer(HomeServer):
|
||||
return JsonResource()
|
||||
|
||||
def build_resource_for_web_client(self):
|
||||
return File("webclient") # TODO configurable?
|
||||
syweb_path = os.path.dirname(syweb.__file__)
|
||||
webclient_path = os.path.join(syweb_path, "webclient")
|
||||
return File(webclient_path) # TODO configurable?
|
||||
|
||||
def build_resource_for_content_repo(self):
|
||||
return ContentRepoResource(self, self.upload_dir, self.auth)
|
||||
return ContentRepoResource(
|
||||
self, self.upload_dir, self.auth, self.content_addr
|
||||
)
|
||||
|
||||
def build_resource_for_server_key(self):
|
||||
return LocalKey(self)
|
||||
|
||||
def build_db_pool(self):
|
||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
||||
don't have to worry about overwriting existing content.
|
||||
"""
|
||||
logging.info("Preparing database: %s...", self.db_name)
|
||||
|
||||
with sqlite3.connect(self.db_name) as db_conn:
|
||||
c = db_conn.cursor()
|
||||
c.execute("PRAGMA user_version")
|
||||
row = c.fetchone()
|
||||
|
||||
if row and row[0]:
|
||||
user_version = row[0]
|
||||
|
||||
if user_version < SCHEMA_VERSION:
|
||||
# TODO(paul): add some kind of intelligent fixup here
|
||||
raise ValueError("Cannot use this database as the " +
|
||||
"schema version (%d) does not match (%d)" %
|
||||
(user_version, SCHEMA_VERSION)
|
||||
)
|
||||
|
||||
else:
|
||||
for sql_loc in SCHEMAS:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
c.executescript(sql_script)
|
||||
db_conn.commit()
|
||||
|
||||
c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
|
||||
|
||||
c.close()
|
||||
|
||||
logging.info("Database prepared in %s.", self.db_name)
|
||||
|
||||
pool = adbapi.ConnectionPool(
|
||||
'sqlite3', self.db_name, check_same_thread=False,
|
||||
cp_min=1, cp_max=1)
|
||||
|
||||
return pool
|
||||
return adbapi.ConnectionPool(
|
||||
"sqlite3", self.get_db_name(),
|
||||
check_same_thread=False,
|
||||
cp_min=1,
|
||||
cp_max=1
|
||||
)
|
||||
|
||||
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
||||
"""Create the resource tree for this Home Server.
|
||||
@@ -133,7 +97,8 @@ class SynapseHomeServer(HomeServer):
|
||||
desired_tree = [
|
||||
(CLIENT_PREFIX, self.get_resource_for_client()),
|
||||
(FEDERATION_PREFIX, self.get_resource_for_federation()),
|
||||
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo())
|
||||
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo()),
|
||||
(SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
|
||||
]
|
||||
if web_client:
|
||||
logger.info("Adding the web client.")
|
||||
@@ -151,7 +116,7 @@ class SynapseHomeServer(HomeServer):
|
||||
# extra resources to existing nodes. See self._resource_id for the key.
|
||||
resource_mappings = {}
|
||||
for (full_path, resource) in desired_tree:
|
||||
logging.info("Attaching %s to path %s", resource, full_path)
|
||||
logger.info("Attaching %s to path %s", resource, full_path)
|
||||
last_resource = self.root_resource
|
||||
for path_seg in full_path.split('/')[1:-1]:
|
||||
if not path_seg in last_resource.listNames():
|
||||
@@ -206,116 +171,82 @@ class SynapseHomeServer(HomeServer):
|
||||
"""
|
||||
return "%s-%s" % (resource, path_seg)
|
||||
|
||||
def start_listening(self, port):
|
||||
reactor.listenTCP(port, Site(self.root_resource))
|
||||
logger.info("Synapse now listening on port %d", port)
|
||||
|
||||
|
||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
||||
""" Sets up logging with verbosity levels.
|
||||
|
||||
Args:
|
||||
verbosity: The verbosity level.
|
||||
filename: Log to the given file rather than to the console.
|
||||
config_path: Path to a python logging config file.
|
||||
"""
|
||||
|
||||
if config_path is None:
|
||||
log_format = (
|
||||
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
level = logging.INFO
|
||||
if verbosity:
|
||||
level = logging.DEBUG
|
||||
|
||||
# FIXME: we need a logging.WARN for a -q quiet option
|
||||
|
||||
logging.basicConfig(level=level, filename=filename, format=log_format)
|
||||
else:
|
||||
logging.config.fileConfig(config_path)
|
||||
|
||||
observer = PythonLoggingObserver()
|
||||
observer.start()
|
||||
|
||||
|
||||
def run():
|
||||
reactor.run()
|
||||
def start_listening(self, secure_port, unsecure_port):
|
||||
if secure_port is not None:
|
||||
reactor.listenSSL(
|
||||
secure_port, Site(self.root_resource), self.tls_context_factory
|
||||
)
|
||||
logger.info("Synapse now listening on port %d", secure_port)
|
||||
if unsecure_port is not None:
|
||||
reactor.listenTCP(
|
||||
unsecure_port, Site(self.root_resource)
|
||||
)
|
||||
logger.info("Synapse now listening on port %d", unsecure_port)
|
||||
|
||||
|
||||
def setup():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
|
||||
help="The port to listen on.")
|
||||
parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
|
||||
help="The database name.")
|
||||
parser.add_argument("-H", "--host", dest="host", default="localhost",
|
||||
help="The hostname of the server.")
|
||||
parser.add_argument('-v', '--verbose', dest="verbose", action='count',
|
||||
help="The verbosity level.")
|
||||
parser.add_argument('-f', '--log-file', dest="log_file", default=None,
|
||||
help="File to log to.")
|
||||
parser.add_argument('--log-config', dest="log_config", default=None,
|
||||
help="Python logging config")
|
||||
parser.add_argument('-D', '--daemonize', action='store_true',
|
||||
default=False, help="Daemonize the home server")
|
||||
parser.add_argument('--pid-file', dest="pid", help="When running as a "
|
||||
"daemon, the file to store the pid in",
|
||||
default="hs.pid")
|
||||
parser.add_argument("-W", "--webclient", dest="webclient", default=True,
|
||||
action="store_false", help="Don't host a web client.")
|
||||
parser.add_argument("--manhole", dest="manhole", type=int, default=None,
|
||||
help="Turn on the twisted telnet manhole service.")
|
||||
args = parser.parse_args()
|
||||
|
||||
verbosity = int(args.verbose) if args.verbose else None
|
||||
|
||||
# Because if/when we daemonize we change to root dir.
|
||||
db_name = os.path.abspath(args.db)
|
||||
log_file = args.log_file
|
||||
if log_file:
|
||||
log_file = os.path.abspath(log_file)
|
||||
|
||||
setup_logging(
|
||||
verbosity=verbosity,
|
||||
filename=log_file,
|
||||
config_path=args.log_config,
|
||||
config = HomeServerConfig.load_config(
|
||||
"Synapse Homeserver",
|
||||
sys.argv[1:],
|
||||
generate_section="Homeserver"
|
||||
)
|
||||
|
||||
logger.info("Server hostname: %s", args.host)
|
||||
config.setup_logging()
|
||||
|
||||
if re.search(":[0-9]+$", args.host):
|
||||
domain_with_port = args.host
|
||||
logger.info("Server hostname: %s", config.server_name)
|
||||
|
||||
if re.search(":[0-9]+$", config.server_name):
|
||||
domain_with_port = config.server_name
|
||||
else:
|
||||
domain_with_port = "%s:%s" % (args.host, args.port)
|
||||
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
|
||||
|
||||
tls_context_factory = context_factory.ServerContextFactory(config)
|
||||
|
||||
hs = SynapseHomeServer(
|
||||
args.host,
|
||||
config.server_name,
|
||||
domain_with_port=domain_with_port,
|
||||
upload_dir=os.path.abspath("uploads"),
|
||||
db_name=db_name,
|
||||
db_name=config.database_path,
|
||||
tls_context_factory=tls_context_factory,
|
||||
config=config,
|
||||
content_addr=config.content_addr,
|
||||
)
|
||||
|
||||
hs.register_servlets()
|
||||
|
||||
hs.create_resource_tree(
|
||||
web_client=args.webclient,
|
||||
redirect_root_to_web_client=True)
|
||||
hs.start_listening(args.port)
|
||||
web_client=config.webclient,
|
||||
redirect_root_to_web_client=True,
|
||||
)
|
||||
|
||||
db_name = hs.get_db_name()
|
||||
|
||||
logger.info("Preparing database: %s...", db_name)
|
||||
|
||||
with sqlite3.connect(db_name) as db_conn:
|
||||
prepare_database(db_conn)
|
||||
|
||||
logger.info("Database prepared in %s.", db_name)
|
||||
|
||||
hs.get_db_pool()
|
||||
|
||||
if args.manhole:
|
||||
if config.manhole:
|
||||
f = twisted.manhole.telnet.ShellFactory()
|
||||
f.username = "matrix"
|
||||
f.password = "rabbithole"
|
||||
f.namespace['hs'] = hs
|
||||
reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
|
||||
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
|
||||
|
||||
if args.daemonize:
|
||||
bind_port = config.bind_port
|
||||
if config.no_tls:
|
||||
bind_port = None
|
||||
hs.start_listening(bind_port, config.unsecure_port)
|
||||
|
||||
if config.daemonize:
|
||||
print config.pid_file
|
||||
daemon = Daemonize(
|
||||
app="synapse-homeserver",
|
||||
pid=args.pid,
|
||||
pid=config.pid_file,
|
||||
action=run,
|
||||
auto_close_fds=False,
|
||||
verbose=True,
|
||||
@@ -324,8 +255,18 @@ def setup():
|
||||
|
||||
daemon.start()
|
||||
else:
|
||||
run()
|
||||
reactor.run()
|
||||
|
||||
|
||||
def run():
|
||||
with LoggingContext("run"):
|
||||
reactor.run()
|
||||
|
||||
|
||||
def main():
|
||||
with LoggingContext("main"):
|
||||
setup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
setup()
|
||||
main()
|
||||
|
||||
70
synapse/app/synctl.py
Executable file
70
synapse/app/synctl.py
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 sys
|
||||
import os
|
||||
import subprocess
|
||||
import signal
|
||||
|
||||
SYNAPSE = ["python", "-m", "synapse.app.homeserver"]
|
||||
|
||||
CONFIGFILE = "homeserver.yaml"
|
||||
PIDFILE = "homeserver.pid"
|
||||
|
||||
GREEN = "\x1b[1;32m"
|
||||
NORMAL = "\x1b[m"
|
||||
|
||||
|
||||
def start():
|
||||
if not os.path.exists(CONFIGFILE):
|
||||
sys.stderr.write(
|
||||
"No config file found\n"
|
||||
"To generate a config file, run '%s -c %s --generate-config"
|
||||
" --server-name=<server name>'\n" % (
|
||||
" ".join(SYNAPSE), CONFIGFILE
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
print "Starting ...",
|
||||
args = SYNAPSE
|
||||
args.extend(["--daemonize", "-c", CONFIGFILE, "--pid-file", PIDFILE])
|
||||
subprocess.check_call(args)
|
||||
print GREEN + "started" + NORMAL
|
||||
|
||||
|
||||
def stop():
|
||||
if os.path.exists(PIDFILE):
|
||||
pid = int(open(PIDFILE).read())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
print GREEN + "stopped" + NORMAL
|
||||
|
||||
|
||||
def main():
|
||||
action = sys.argv[1] if sys.argv[1:] else "usage"
|
||||
if action == "start":
|
||||
start()
|
||||
elif action == "stop":
|
||||
stop()
|
||||
elif action == "restart":
|
||||
stop()
|
||||
start()
|
||||
else:
|
||||
sys.stderr.write("Usage: %s [start|stop|restart]\n" % (sys.argv[0],))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
143
synapse/config/_base.py
Normal file
143
synapse/config/_base.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 argparse
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Config(object):
|
||||
def __init__(self, args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def abspath(file_path):
|
||||
return os.path.abspath(file_path) if file_path else file_path
|
||||
|
||||
@classmethod
|
||||
def check_file(cls, file_path, config_name):
|
||||
if file_path is None:
|
||||
raise ConfigError(
|
||||
"Missing config for %s."
|
||||
" You must specify a path for the config file. You can "
|
||||
"do this with the -c or --config-path option. "
|
||||
"Adding --generate-config along with --server-name "
|
||||
"<server name> will generate a config file at the given path."
|
||||
% (config_name,)
|
||||
)
|
||||
if not os.path.exists(file_path):
|
||||
raise ConfigError(
|
||||
"File % config for %s doesn't exist."
|
||||
" Try running again with --generate-config"
|
||||
% (config_name,)
|
||||
)
|
||||
return cls.abspath(file_path)
|
||||
|
||||
@classmethod
|
||||
def read_file(cls, file_path, config_name):
|
||||
cls.check_file(file_path, config_name)
|
||||
with open(file_path) as file_stream:
|
||||
return file_stream.read()
|
||||
|
||||
@staticmethod
|
||||
def read_config_file(file_path):
|
||||
with open(file_path) as file_stream:
|
||||
return yaml.load(file_stream)
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def generate_config(cls, args, config_dir_path):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def load_config(cls, description, argv, generate_section=None):
|
||||
config_parser = argparse.ArgumentParser(add_help=False)
|
||||
config_parser.add_argument(
|
||||
"-c", "--config-path",
|
||||
metavar="CONFIG_FILE",
|
||||
help="Specify config file"
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--generate-config",
|
||||
action="store_true",
|
||||
help="Generate config file"
|
||||
)
|
||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||
|
||||
if config_args.generate_config:
|
||||
if not config_args.config_path:
|
||||
config_parser.error(
|
||||
"Must specify where to generate the config file"
|
||||
)
|
||||
config_dir_path = os.path.dirname(config_args.config_path)
|
||||
if os.path.exists(config_args.config_path):
|
||||
defaults = cls.read_config_file(config_args.config_path)
|
||||
else:
|
||||
defaults = {}
|
||||
else:
|
||||
if config_args.config_path:
|
||||
defaults = cls.read_config_file(config_args.config_path)
|
||||
else:
|
||||
defaults = {}
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
parents=[config_parser],
|
||||
description=description,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
cls.add_arguments(parser)
|
||||
parser.set_defaults(**defaults)
|
||||
|
||||
args = parser.parse_args(remaining_args)
|
||||
|
||||
if config_args.generate_config:
|
||||
config_dir_path = os.path.dirname(config_args.config_path)
|
||||
config_dir_path = os.path.abspath(config_dir_path)
|
||||
if not os.path.exists(config_dir_path):
|
||||
os.makedirs(config_dir_path)
|
||||
cls.generate_config(args, config_dir_path)
|
||||
config = {}
|
||||
for key, value in vars(args).items():
|
||||
if (key not in set(["config_path", "generate_config"])
|
||||
and value is not None):
|
||||
config[key] = value
|
||||
with open(config_args.config_path, "w") as config_file:
|
||||
# TODO(paul) it would be lovely if we wrote out vim- and emacs-
|
||||
# style mode markers into the file, to hint to people that
|
||||
# this is a YAML file.
|
||||
yaml.dump(config, config_file, default_flow_style=False)
|
||||
print (
|
||||
"A config file has been generated in %s for server name"
|
||||
" '%s' with corresponding SSL keys and self-signed"
|
||||
" certificates. Please review this file and customise it to"
|
||||
" your needs."
|
||||
) % (
|
||||
config_args.config_path, config['server_name']
|
||||
)
|
||||
print (
|
||||
"If this server name is incorrect, you will need to regenerate"
|
||||
" the SSL certificates"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
return cls(args)
|
||||
51
synapse/config/captcha.py
Normal file
51
synapse/config/captcha.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Copyright 2014 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 CaptchaConfig(Config):
|
||||
|
||||
def __init__(self, args):
|
||||
super(CaptchaConfig, self).__init__(args)
|
||||
self.recaptcha_private_key = args.recaptcha_private_key
|
||||
self.enable_registration_captcha = args.enable_registration_captcha
|
||||
self.captcha_ip_origin_is_x_forwarded = (
|
||||
args.captcha_ip_origin_is_x_forwarded
|
||||
)
|
||||
self.captcha_bypass_secret = args.captcha_bypass_secret
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(CaptchaConfig, cls).add_arguments(parser)
|
||||
group = parser.add_argument_group("recaptcha")
|
||||
group.add_argument(
|
||||
"--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY",
|
||||
help="The matching private key for the web client's public key."
|
||||
)
|
||||
group.add_argument(
|
||||
"--enable-registration-captcha", type=bool, default=False,
|
||||
help="Enables ReCaptcha checks when registering, preventing signup"
|
||||
+ " unless a captcha is answered. Requires a valid ReCaptcha "
|
||||
+ "public/private key."
|
||||
)
|
||||
group.add_argument(
|
||||
"--captcha_ip_origin_is_x_forwarded", type=bool, default=False,
|
||||
help="When checking captchas, use the X-Forwarded-For (XFF) header"
|
||||
+ " as the client IP and not the actual client IP."
|
||||
)
|
||||
group.add_argument(
|
||||
"--captcha_bypass_secret", type=str,
|
||||
help="A secret key used to bypass the captcha test entirely."
|
||||
)
|
||||
37
synapse/config/database.py
Normal file
37
synapse/config/database.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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
|
||||
import os
|
||||
|
||||
|
||||
class DatabaseConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(DatabaseConfig, self).__init__(args)
|
||||
self.database_path = self.abspath(args.database_path)
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(DatabaseConfig, cls).add_arguments(parser)
|
||||
db_group = parser.add_argument_group("database")
|
||||
db_group.add_argument(
|
||||
"-d", "--database-path", default="homeserver.db",
|
||||
help="The database name."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_config(cls, args, config_dir_path):
|
||||
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
|
||||
args.database_path = os.path.abspath(args.database_path)
|
||||
42
synapse/config/email.py
Normal file
42
synapse/config/email.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 EmailConfig(Config):
|
||||
|
||||
def __init__(self, args):
|
||||
super(EmailConfig, self).__init__(args)
|
||||
self.email_from_address = args.email_from_address
|
||||
self.email_smtp_server = args.email_smtp_server
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(EmailConfig, cls).add_arguments(parser)
|
||||
email_group = parser.add_argument_group("email")
|
||||
email_group.add_argument(
|
||||
"--email-from-address",
|
||||
default="FROM@EXAMPLE.COM",
|
||||
help="The address to send emails from (e.g. for password resets)."
|
||||
)
|
||||
email_group.add_argument(
|
||||
"--email-smtp-server",
|
||||
default="",
|
||||
help=(
|
||||
"The SMTP server to send emails from (e.g. for password"
|
||||
" resets)."
|
||||
)
|
||||
)
|
||||
35
synapse/config/homeserver.py
Normal file
35
synapse/config/homeserver.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 .tls import TlsConfig
|
||||
from .server import ServerConfig
|
||||
from .logger import LoggingConfig
|
||||
from .database import DatabaseConfig
|
||||
from .ratelimiting import RatelimitConfig
|
||||
from .repository import ContentRepositoryConfig
|
||||
from .captcha import CaptchaConfig
|
||||
from .email import EmailConfig
|
||||
from .voip import VoipConfig
|
||||
|
||||
|
||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||
EmailConfig, VoipConfig):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
||||
76
synapse/config/logger.py
Normal file
76
synapse/config/logger.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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
|
||||
from synapse.util.logcontext import LoggingContextFilter
|
||||
from twisted.python.log import PythonLoggingObserver
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
|
||||
class LoggingConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(LoggingConfig, self).__init__(args)
|
||||
self.verbosity = int(args.verbose) if args.verbose else None
|
||||
self.log_config = self.abspath(args.log_config)
|
||||
self.log_file = self.abspath(args.log_file)
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(LoggingConfig, cls).add_arguments(parser)
|
||||
logging_group = parser.add_argument_group("logging")
|
||||
logging_group.add_argument(
|
||||
'-v', '--verbose', dest="verbose", action='count',
|
||||
help="The verbosity level."
|
||||
)
|
||||
logging_group.add_argument(
|
||||
'-f', '--log-file', dest="log_file", default=None,
|
||||
help="File to log to."
|
||||
)
|
||||
logging_group.add_argument(
|
||||
'--log-config', dest="log_config", default=None,
|
||||
help="Python logging config file"
|
||||
)
|
||||
|
||||
def setup_logging(self):
|
||||
log_format = (
|
||||
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
||||
" - %(message)s"
|
||||
)
|
||||
if self.log_config is None:
|
||||
|
||||
level = logging.INFO
|
||||
if self.verbosity:
|
||||
level = logging.DEBUG
|
||||
|
||||
# FIXME: we need a logging.WARN for a -q quiet option
|
||||
logger = logging.getLogger('')
|
||||
logger.setLevel(level)
|
||||
formatter = logging.Formatter(log_format)
|
||||
if self.log_file:
|
||||
handler = logging.FileHandler(self.log_file)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
handler.addFilter(LoggingContextFilter(request=""))
|
||||
|
||||
logger.addHandler(handler)
|
||||
logger.info("Test")
|
||||
else:
|
||||
logging.config.fileConfig(self.log_config)
|
||||
|
||||
observer = PythonLoggingObserver()
|
||||
observer.start()
|
||||
36
synapse/config/ratelimiting.py
Normal file
36
synapse/config/ratelimiting.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Copyright 2014 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 RatelimitConfig(Config):
|
||||
|
||||
def __init__(self, args):
|
||||
super(RatelimitConfig, self).__init__(args)
|
||||
self.rc_messages_per_second = args.rc_messages_per_second
|
||||
self.rc_message_burst_count = args.rc_message_burst_count
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(RatelimitConfig, cls).add_arguments(parser)
|
||||
rc_group = parser.add_argument_group("ratelimiting")
|
||||
rc_group.add_argument(
|
||||
"--rc-messages-per-second", type=float, default=0.2,
|
||||
help="number of messages a client can send per second"
|
||||
)
|
||||
rc_group.add_argument(
|
||||
"--rc-message-burst-count", type=float, default=10,
|
||||
help="number of message a client can send before being throttled"
|
||||
)
|
||||
39
synapse/config/repository.py
Normal file
39
synapse/config/repository.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# 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 ContentRepositoryConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(ContentRepositoryConfig, self).__init__(args)
|
||||
self.max_upload_size = self.parse_size(args.max_upload_size)
|
||||
|
||||
def parse_size(self, string):
|
||||
sizes = {"K": 1024, "M": 1024 * 1024}
|
||||
size = 1
|
||||
suffix = string[-1]
|
||||
if suffix in sizes:
|
||||
string = string[:-1]
|
||||
size = sizes[suffix]
|
||||
return int(string) * size
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(ContentRepositoryConfig, cls).add_arguments(parser)
|
||||
db_group = parser.add_argument_group("content_repository")
|
||||
db_group.add_argument(
|
||||
"--max-upload-size", default="1M"
|
||||
)
|
||||
118
synapse/config/server.py
Normal file
118
synapse/config/server.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 os
|
||||
from ._base import Config, ConfigError
|
||||
import syutil.crypto.signing_key
|
||||
|
||||
|
||||
class ServerConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(ServerConfig, self).__init__(args)
|
||||
self.server_name = args.server_name
|
||||
self.signing_key = self.read_signing_key(args.signing_key_path)
|
||||
self.bind_port = args.bind_port
|
||||
self.bind_host = args.bind_host
|
||||
self.unsecure_port = args.unsecure_port
|
||||
self.daemonize = args.daemonize
|
||||
self.pid_file = self.abspath(args.pid_file)
|
||||
self.webclient = True
|
||||
self.manhole = args.manhole
|
||||
self.no_tls = args.no_tls
|
||||
|
||||
if not args.content_addr:
|
||||
host = args.server_name
|
||||
if ':' not in host:
|
||||
host = "%s:%d" % (host, args.unsecure_port)
|
||||
else:
|
||||
host = host.split(':')[0]
|
||||
host = "%s:%d" % (host, args.unsecure_port)
|
||||
args.content_addr = "http://%s" % (host,)
|
||||
|
||||
self.content_addr = args.content_addr
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(ServerConfig, cls).add_arguments(parser)
|
||||
server_group = parser.add_argument_group("server")
|
||||
server_group.add_argument("-H", "--server-name", default="localhost",
|
||||
help="The name of the server")
|
||||
server_group.add_argument("--signing-key-path",
|
||||
help="The signing key to sign messages with")
|
||||
server_group.add_argument("-p", "--bind-port", metavar="PORT",
|
||||
type=int, help="https port to listen on",
|
||||
default=8448)
|
||||
server_group.add_argument("--unsecure-port", metavar="PORT",
|
||||
type=int, help="http port to listen on",
|
||||
default=8008)
|
||||
server_group.add_argument("--bind-host", default="",
|
||||
help="Local interface to listen on")
|
||||
server_group.add_argument("-D", "--daemonize", action='store_true',
|
||||
help="Daemonize the home server")
|
||||
server_group.add_argument('--pid-file', default="homeserver.pid",
|
||||
help="When running as a daemon, the file to"
|
||||
" store the pid in")
|
||||
server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
|
||||
type=int,
|
||||
help="Turn on the twisted telnet manhole"
|
||||
" service on the given port.")
|
||||
server_group.add_argument("--content-addr", default=None,
|
||||
help="The host and scheme to use for the "
|
||||
"content repository")
|
||||
server_group.add_argument("--no-tls", action='store_true',
|
||||
help="Don't bind to the https port.")
|
||||
|
||||
def read_signing_key(self, signing_key_path):
|
||||
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||
try:
|
||||
return syutil.crypto.signing_key.read_signing_keys(
|
||||
signing_keys.splitlines(True)
|
||||
)
|
||||
except Exception:
|
||||
raise ConfigError(
|
||||
"Error reading signing_key."
|
||||
" Try running again with --generate-config"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_config(cls, args, config_dir_path):
|
||||
super(ServerConfig, cls).generate_config(args, config_dir_path)
|
||||
base_key_name = os.path.join(config_dir_path, args.server_name)
|
||||
|
||||
args.pid_file = os.path.abspath(args.pid_file)
|
||||
|
||||
if not args.signing_key_path:
|
||||
args.signing_key_path = base_key_name + ".signing.key"
|
||||
|
||||
if not os.path.exists(args.signing_key_path):
|
||||
with open(args.signing_key_path, "w") as signing_key_file:
|
||||
syutil.crypto.signing_key.write_signing_keys(
|
||||
signing_key_file,
|
||||
(syutil.crypto.signing_key.generate_singing_key("auto"),),
|
||||
)
|
||||
else:
|
||||
signing_keys = cls.read_file(args.signing_key_path, "signing_key")
|
||||
if len(signing_keys.split("\n")[0].split()) == 1:
|
||||
# handle keys in the old format.
|
||||
key = syutil.crypto.signing_key.decode_signing_key_base64(
|
||||
syutil.crypto.signing_key.NACL_ED25519,
|
||||
"auto",
|
||||
signing_keys.split("\n")[0]
|
||||
)
|
||||
with open(args.signing_key_path, "w") as signing_key_file:
|
||||
syutil.crypto.signing_key.write_signing_keys(
|
||||
signing_key_file,
|
||||
(key,),
|
||||
)
|
||||
130
synapse/config/tls.py
Normal file
130
synapse/config/tls.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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
|
||||
|
||||
from OpenSSL import crypto
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
GENERATE_DH_PARAMS = False
|
||||
|
||||
|
||||
class TlsConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(TlsConfig, self).__init__(args)
|
||||
self.tls_certificate = self.read_tls_certificate(
|
||||
args.tls_certificate_path
|
||||
)
|
||||
self.tls_private_key = self.read_tls_private_key(
|
||||
args.tls_private_key_path
|
||||
)
|
||||
self.tls_dh_params_path = self.check_file(
|
||||
args.tls_dh_params_path, "tls_dh_params"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(TlsConfig, cls).add_arguments(parser)
|
||||
tls_group = parser.add_argument_group("tls")
|
||||
tls_group.add_argument("--tls-certificate-path",
|
||||
help="PEM encoded X509 certificate for TLS")
|
||||
tls_group.add_argument("--tls-private-key-path",
|
||||
help="PEM encoded private key for TLS")
|
||||
tls_group.add_argument("--tls-dh-params-path",
|
||||
help="PEM dh parameters for ephemeral keys")
|
||||
|
||||
def read_tls_certificate(self, cert_path):
|
||||
cert_pem = self.read_file(cert_path, "tls_certificate")
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
||||
|
||||
def read_tls_private_key(self, private_key_path):
|
||||
private_key_pem = self.read_file(private_key_path, "tls_private_key")
|
||||
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
||||
|
||||
@classmethod
|
||||
def generate_config(cls, args, config_dir_path):
|
||||
super(TlsConfig, cls).generate_config(args, config_dir_path)
|
||||
base_key_name = os.path.join(config_dir_path, args.server_name)
|
||||
|
||||
if args.tls_certificate_path is None:
|
||||
args.tls_certificate_path = base_key_name + ".tls.crt"
|
||||
|
||||
if args.tls_private_key_path is None:
|
||||
args.tls_private_key_path = base_key_name + ".tls.key"
|
||||
|
||||
if args.tls_dh_params_path is None:
|
||||
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
||||
|
||||
if not os.path.exists(args.tls_private_key_path):
|
||||
with open(args.tls_private_key_path, "w") as private_key_file:
|
||||
tls_private_key = crypto.PKey()
|
||||
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
||||
private_key_pem = crypto.dump_privatekey(
|
||||
crypto.FILETYPE_PEM, tls_private_key
|
||||
)
|
||||
private_key_file.write(private_key_pem)
|
||||
else:
|
||||
with open(args.tls_private_key_path) as private_key_file:
|
||||
private_key_pem = private_key_file.read()
|
||||
tls_private_key = crypto.load_privatekey(
|
||||
crypto.FILETYPE_PEM, private_key_pem
|
||||
)
|
||||
|
||||
if not os.path.exists(args.tls_certificate_path):
|
||||
with open(args.tls_certificate_path, "w") as certifcate_file:
|
||||
cert = crypto.X509()
|
||||
subject = cert.get_subject()
|
||||
subject.CN = args.server_name
|
||||
|
||||
cert.set_serial_number(1000)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
||||
cert.set_issuer(cert.get_subject())
|
||||
cert.set_pubkey(tls_private_key)
|
||||
|
||||
cert.sign(tls_private_key, 'sha256')
|
||||
|
||||
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
|
||||
certifcate_file.write(cert_pem)
|
||||
|
||||
if not os.path.exists(args.tls_dh_params_path):
|
||||
if GENERATE_DH_PARAMS:
|
||||
subprocess.check_call([
|
||||
"openssl", "dhparam",
|
||||
"-outform", "PEM",
|
||||
"-out", args.tls_dh_params_path,
|
||||
"2048"
|
||||
])
|
||||
else:
|
||||
with open(args.tls_dh_params_path, "w") as dh_params_file:
|
||||
dh_params_file.write(
|
||||
"2048-bit DH parameters taken from rfc3526\n"
|
||||
"-----BEGIN DH PARAMETERS-----\n"
|
||||
"MIIBCAKCAQEA///////////JD9qiIWjC"
|
||||
"NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n"
|
||||
"IlFKCHmONATd75UZs806QxswKwpt8l8U"
|
||||
"N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n"
|
||||
"awv/XLb0Brft7jhr+1qJn6WunyQRfEsf"
|
||||
"5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n"
|
||||
"mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS"
|
||||
"u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n"
|
||||
"fDKQXkYuNs474553LBgOhgObJ4Oi7Aei"
|
||||
"j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n"
|
||||
"5RXSJhiY+gUQFXKOWoqsqmj/////////"
|
||||
"/wIBAg==\n"
|
||||
"-----END DH PARAMETERS-----\n"
|
||||
)
|
||||
44
synapse/config/voip.py
Normal file
44
synapse/config/voip.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright 2014 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 VoipConfig(Config):
|
||||
|
||||
def __init__(self, args):
|
||||
super(VoipConfig, self).__init__(args)
|
||||
self.turn_uris = args.turn_uris
|
||||
self.turn_shared_secret = args.turn_shared_secret
|
||||
self.turn_user_lifetime = args.turn_user_lifetime
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
super(VoipConfig, cls).add_arguments(parser)
|
||||
group = parser.add_argument_group("voip")
|
||||
group.add_argument(
|
||||
"--turn-uris", type=str, default=None,
|
||||
help="The public URIs of the TURN server to give to clients"
|
||||
)
|
||||
group.add_argument(
|
||||
"--turn-shared-secret", type=str, default=None,
|
||||
help=(
|
||||
"The shared secret used to compute passwords for the TURN"
|
||||
" server"
|
||||
)
|
||||
)
|
||||
group.add_argument(
|
||||
"--turn-user-lifetime", type=int, default=(1000 * 60 * 60),
|
||||
help="How long generated TURN credentials last, in ms"
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# 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 ConfigParser as configparser
|
||||
import argparse
|
||||
import socket
|
||||
import sys
|
||||
import os
|
||||
from OpenSSL import crypto
|
||||
import nacl.signing
|
||||
from syutil.base64util import encode_base64
|
||||
import subprocess
|
||||
|
||||
|
||||
def load_config(description, argv):
|
||||
config_parser = argparse.ArgumentParser(add_help=False)
|
||||
config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE",
|
||||
help="Specify config file")
|
||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||
if config_args.config_path:
|
||||
config = configparser.SafeConfigParser()
|
||||
config.read([config_args.config_path])
|
||||
defaults = dict(config.items("KeyServer"))
|
||||
else:
|
||||
defaults = {}
|
||||
parser = argparse.ArgumentParser(
|
||||
parents=[config_parser],
|
||||
description=description,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.set_defaults(**defaults)
|
||||
parser.add_argument("--server-name", default=socket.getfqdn(),
|
||||
help="The name of the server")
|
||||
parser.add_argument("--signing-key-path",
|
||||
help="The signing key to sign responses with")
|
||||
parser.add_argument("--tls-certificate-path",
|
||||
help="PEM encoded X509 certificate for TLS")
|
||||
parser.add_argument("--tls-private-key-path",
|
||||
help="PEM encoded private key for TLS")
|
||||
parser.add_argument("--tls-dh-params-path",
|
||||
help="PEM encoded dh parameters for ephemeral keys")
|
||||
parser.add_argument("--bind-port", type=int,
|
||||
help="TCP port to listen on")
|
||||
parser.add_argument("--bind-host", default="",
|
||||
help="Local interface to listen on")
|
||||
|
||||
args = parser.parse_args(remaining_args)
|
||||
|
||||
server_config = vars(args)
|
||||
del server_config["config_path"]
|
||||
return server_config
|
||||
|
||||
|
||||
def generate_config(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-c", "--config-path", help="Specify config file",
|
||||
metavar="CONFIG_FILE", required=True)
|
||||
parser.add_argument("--server-name", default=socket.getfqdn(),
|
||||
help="The name of the server")
|
||||
parser.add_argument("--signing-key-path",
|
||||
help="The signing key to sign responses with")
|
||||
parser.add_argument("--tls-certificate-path",
|
||||
help="PEM encoded X509 certificate for TLS")
|
||||
parser.add_argument("--tls-private-key-path",
|
||||
help="PEM encoded private key for TLS")
|
||||
parser.add_argument("--tls-dh-params-path",
|
||||
help="PEM encoded dh parameters for ephemeral keys")
|
||||
parser.add_argument("--bind-port", type=int, required=True,
|
||||
help="TCP port to listen on")
|
||||
parser.add_argument("--bind-host", default="",
|
||||
help="Local interface to listen on")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
dir_name = os.path.dirname(args.config_path)
|
||||
base_key_name = os.path.join(dir_name, args.server_name)
|
||||
|
||||
if args.signing_key_path is None:
|
||||
args.signing_key_path = base_key_name + ".signing.key"
|
||||
|
||||
if args.tls_certificate_path is None:
|
||||
args.tls_certificate_path = base_key_name + ".tls.crt"
|
||||
|
||||
if args.tls_private_key_path is None:
|
||||
args.tls_private_key_path = base_key_name + ".tls.key"
|
||||
|
||||
if args.tls_dh_params_path is None:
|
||||
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
||||
|
||||
if not os.path.exists(args.signing_key_path):
|
||||
with open(args.signing_key_path, "w") as signing_key_file:
|
||||
key = nacl.signing.SigningKey.generate()
|
||||
signing_key_file.write(encode_base64(key.encode()))
|
||||
|
||||
if not os.path.exists(args.tls_private_key_path):
|
||||
with open(args.tls_private_key_path, "w") as private_key_file:
|
||||
tls_private_key = crypto.PKey()
|
||||
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
||||
private_key_pem = crypto.dump_privatekey(
|
||||
crypto.FILETYPE_PEM, tls_private_key
|
||||
)
|
||||
private_key_file.write(private_key_pem)
|
||||
else:
|
||||
with open(args.tls_private_key_path) as private_key_file:
|
||||
private_key_pem = private_key_file.read()
|
||||
tls_private_key = crypto.load_privatekey(
|
||||
crypto.FILETYPE_PEM, private_key_pem
|
||||
)
|
||||
|
||||
if not os.path.exists(args.tls_certificate_path):
|
||||
with open(args.tls_certificate_path, "w") as certifcate_file:
|
||||
cert = crypto.X509()
|
||||
subject = cert.get_subject()
|
||||
subject.CN = args.server_name
|
||||
|
||||
cert.set_serial_number(1000)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
||||
cert.set_issuer(cert.get_subject())
|
||||
cert.set_pubkey(tls_private_key)
|
||||
|
||||
cert.sign(tls_private_key, 'sha256')
|
||||
|
||||
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
|
||||
certifcate_file.write(cert_pem)
|
||||
|
||||
if not os.path.exists(args.tls_dh_params_path):
|
||||
subprocess.check_call([
|
||||
"openssl", "dhparam",
|
||||
"-outform", "PEM",
|
||||
"-out", args.tls_dh_params_path,
|
||||
"2048"
|
||||
])
|
||||
|
||||
config = configparser.SafeConfigParser()
|
||||
config.add_section("KeyServer")
|
||||
for key, value in vars(args).items():
|
||||
if key != "config_path":
|
||||
config.set("KeyServer", key, str(value))
|
||||
|
||||
with open(args.config_path, "w") as config_file:
|
||||
config.write(config_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_config(sys.argv[1:])
|
||||
46
synapse/crypto/context_factory.py
Normal file
46
synapse/crypto/context_factory.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Copyright 2014 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 ssl
|
||||
from OpenSSL import SSL
|
||||
from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerContextFactory(ssl.ContextFactory):
|
||||
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
||||
connections and to make connections to remote servers."""
|
||||
|
||||
def __init__(self, config):
|
||||
self._context = SSL.Context(SSL.SSLv23_METHOD)
|
||||
self.configure_context(self._context, config)
|
||||
|
||||
@staticmethod
|
||||
def configure_context(context, config):
|
||||
try:
|
||||
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
|
||||
_ecCurve.addECKeyToContext(context)
|
||||
except:
|
||||
logger.exception("Failed to enable eliptic curve for TLS")
|
||||
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||
context.use_certificate(config.tls_certificate)
|
||||
context.use_privatekey(config.tls_private_key)
|
||||
context.load_tmp_dh(config.tls_dh_params_path)
|
||||
context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
|
||||
|
||||
def getContext(self):
|
||||
return self._context
|
||||
108
synapse/crypto/event_signing.py
Normal file
108
synapse/crypto/event_signing.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 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 synapse.api.events.utils import prune_event
|
||||
from syutil.jsonutil import encode_canonical_json
|
||||
from syutil.base64util import encode_base64, decode_base64
|
||||
from syutil.crypto.jsonsign import sign_json
|
||||
from synapse.api.errors import SynapseError, Codes
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
|
||||
"""Check whether the hash for this PDU matches the contents"""
|
||||
computed_hash = _compute_content_hash(event, hash_algorithm)
|
||||
logger.debug("Expecting hash: %s", encode_base64(computed_hash.digest()))
|
||||
if computed_hash.name not in event.hashes:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Algorithm %s not in hashes %s" % (
|
||||
computed_hash.name, list(event.hashes),
|
||||
),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
message_hash_base64 = event.hashes[computed_hash.name]
|
||||
try:
|
||||
message_hash_bytes = decode_base64(message_hash_base64)
|
||||
except:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Invalid base64: %s" % (message_hash_base64,),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
return message_hash_bytes == computed_hash.digest()
|
||||
|
||||
|
||||
def _compute_content_hash(event, hash_algorithm):
|
||||
event_json = event.get_pdu_json()
|
||||
event_json.pop("age_ts", None)
|
||||
event_json.pop("unsigned", None)
|
||||
event_json.pop("signatures", None)
|
||||
event_json.pop("hashes", None)
|
||||
event_json.pop("outlier", None)
|
||||
event_json.pop("destinations", None)
|
||||
event_json_bytes = encode_canonical_json(event_json)
|
||||
return hash_algorithm(event_json_bytes)
|
||||
|
||||
|
||||
def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256):
|
||||
tmp_event = prune_event(event)
|
||||
event_json = tmp_event.get_pdu_json()
|
||||
event_json.pop("signatures", None)
|
||||
event_json.pop("age_ts", None)
|
||||
event_json.pop("unsigned", None)
|
||||
event_json_bytes = encode_canonical_json(event_json)
|
||||
hashed = hash_algorithm(event_json_bytes)
|
||||
return (hashed.name, hashed.digest())
|
||||
|
||||
|
||||
def compute_event_signature(event, signature_name, signing_key):
|
||||
tmp_event = prune_event(event)
|
||||
redact_json = tmp_event.get_pdu_json()
|
||||
redact_json.pop("age_ts", None)
|
||||
redact_json.pop("unsigned", None)
|
||||
logger.debug("Signing event: %s", redact_json)
|
||||
redact_json = sign_json(redact_json, signature_name, signing_key)
|
||||
return redact_json["signatures"]
|
||||
|
||||
|
||||
def add_hashes_and_signatures(event, signature_name, signing_key,
|
||||
hash_algorithm=hashlib.sha256):
|
||||
if hasattr(event, "old_state_events"):
|
||||
state_json_bytes = encode_canonical_json(
|
||||
[e.event_id for e in event.old_state_events.values()]
|
||||
)
|
||||
hashed = hash_algorithm(state_json_bytes)
|
||||
event.state_hash = {
|
||||
hashed.name: encode_base64(hashed.digest())
|
||||
}
|
||||
|
||||
hashed = _compute_content_hash(event, hash_algorithm=hash_algorithm)
|
||||
|
||||
if not hasattr(event, "hashes"):
|
||||
event.hashes = {}
|
||||
event.hashes[hashed.name] = encode_base64(hashed.digest())
|
||||
|
||||
event.signatures = compute_event_signature(
|
||||
event,
|
||||
signature_name=signature_name,
|
||||
signing_key=signing_key,
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,9 +15,10 @@
|
||||
|
||||
|
||||
from twisted.web.http import HTTPClient
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.protocol import ClientFactory
|
||||
from twisted.names.srvconnect import SRVConnector
|
||||
from synapse.http.endpoint import matrix_federation_endpoint
|
||||
from synapse.util.logcontext import PreserveLoggingContext
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -30,19 +31,24 @@ def fetch_server_key(server_name, ssl_context_factory):
|
||||
"""Fetch the keys for a remote server."""
|
||||
|
||||
factory = SynapseKeyClientFactory()
|
||||
endpoint = matrix_federation_endpoint(
|
||||
reactor, server_name, ssl_context_factory, timeout=30
|
||||
)
|
||||
|
||||
SRVConnector(
|
||||
reactor, "matrix", server_name, factory,
|
||||
protocol="tcp", connectFuncName="connectSSL", defaultPort=443,
|
||||
connectFuncKwArgs=dict(contextFactory=ssl_context_factory)).connect()
|
||||
|
||||
server_key, server_certificate = yield factory.remote_key
|
||||
|
||||
defer.returnValue((server_key, server_certificate))
|
||||
for i in range(5):
|
||||
try:
|
||||
with PreserveLoggingContext():
|
||||
protocol = yield endpoint.connect(factory)
|
||||
server_response, server_certificate = yield protocol.remote_key
|
||||
defer.returnValue((server_response, server_certificate))
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
raise IOError("Cannot get key for %s" % server_name)
|
||||
|
||||
|
||||
class SynapseKeyClientError(Exception):
|
||||
"""The key wasn't retireved from the remote server."""
|
||||
"""The key wasn't retrieved from the remote server."""
|
||||
pass
|
||||
|
||||
|
||||
@@ -51,69 +57,46 @@ class SynapseKeyClientProtocol(HTTPClient):
|
||||
the server and extracts the X.509 certificate for the remote peer from the
|
||||
SSL connection."""
|
||||
|
||||
timeout = 30
|
||||
|
||||
def __init__(self):
|
||||
self.remote_key = defer.Deferred()
|
||||
|
||||
def connectionMade(self):
|
||||
logger.debug("Connected to %s", self.transport.getHost())
|
||||
self.sendCommand(b"GET", b"/key")
|
||||
self.sendCommand(b"GET", b"/_matrix/key/v1/")
|
||||
self.endHeaders()
|
||||
self.timer = reactor.callLater(
|
||||
self.factory.timeout_seconds,
|
||||
self.timeout,
|
||||
self.on_timeout
|
||||
)
|
||||
|
||||
def handleStatus(self, version, status, message):
|
||||
if status != b"200":
|
||||
logger.info("Non-200 response from %s: %s %s",
|
||||
self.transport.getHost(), status, message)
|
||||
#logger.info("Non-200 response from %s: %s %s",
|
||||
# self.transport.getHost(), status, message)
|
||||
self.transport.abortConnection()
|
||||
|
||||
def handleResponse(self, response_body_bytes):
|
||||
try:
|
||||
json_response = json.loads(response_body_bytes)
|
||||
except ValueError:
|
||||
logger.info("Invalid JSON response from %s",
|
||||
self.transport.getHost())
|
||||
#logger.info("Invalid JSON response from %s",
|
||||
# self.transport.getHost())
|
||||
self.transport.abortConnection()
|
||||
return
|
||||
|
||||
certificate = self.transport.getPeerCertificate()
|
||||
self.factory.on_remote_key((json_response, certificate))
|
||||
self.remote_key.callback((json_response, certificate))
|
||||
self.transport.abortConnection()
|
||||
self.timer.cancel()
|
||||
|
||||
def on_timeout(self):
|
||||
logger.debug("Timeout waiting for response from %s",
|
||||
self.transport.getHost())
|
||||
self.remote_key.errback(IOError("Timeout waiting for response"))
|
||||
self.transport.abortConnection()
|
||||
|
||||
|
||||
class SynapseKeyClientFactory(ClientFactory):
|
||||
class SynapseKeyClientFactory(Factory):
|
||||
protocol = SynapseKeyClientProtocol
|
||||
max_retries = 5
|
||||
timeout_seconds = 30
|
||||
|
||||
def __init__(self):
|
||||
self.succeeded = False
|
||||
self.retries = 0
|
||||
self.remote_key = defer.Deferred()
|
||||
|
||||
def on_remote_key(self, key):
|
||||
self.succeeded = True
|
||||
self.remote_key.callback(key)
|
||||
|
||||
def retry_connection(self, connector):
|
||||
self.retries += 1
|
||||
if self.retries < self.max_retries:
|
||||
connector.connector = None
|
||||
connector.connect()
|
||||
else:
|
||||
self.remote_key.errback(
|
||||
SynapseKeyClientError("Max retries exceeded"))
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
logger.info("Connection failed %s", reason)
|
||||
self.retry_connection(connector)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
logger.info("Connection lost %s", reason)
|
||||
if not self.succeeded:
|
||||
self.retry_connection(connector)
|
||||
|
||||
155
synapse/crypto/keyring.py
Normal file
155
synapse/crypto/keyring.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 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 synapse.crypto.keyclient import fetch_server_key
|
||||
from twisted.internet import defer
|
||||
from syutil.crypto.jsonsign import verify_signed_json, signature_ids
|
||||
from syutil.crypto.signing_key import (
|
||||
is_signing_algorithm_supported, decode_verify_key_bytes
|
||||
)
|
||||
from syutil.base64util import decode_base64, encode_base64
|
||||
from synapse.api.errors import SynapseError, Codes
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Keyring(object):
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.clock = hs.get_clock()
|
||||
self.hs = hs
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def verify_json_for_server(self, server_name, json_object):
|
||||
logger.debug("Verifying for %s", server_name)
|
||||
key_ids = signature_ids(json_object, server_name)
|
||||
if not key_ids:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Not signed with a supported algorithm",
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
try:
|
||||
verify_key = yield self.get_server_verify_key(server_name, key_ids)
|
||||
except IOError:
|
||||
raise SynapseError(
|
||||
502,
|
||||
"Error downloading keys for %s" % (server_name,),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
except:
|
||||
raise SynapseError(
|
||||
401,
|
||||
"No key for %s with id %s" % (server_name, key_ids),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
try:
|
||||
verify_signed_json(json_object, server_name, verify_key)
|
||||
except:
|
||||
raise SynapseError(
|
||||
401,
|
||||
"Invalid signature for server %s with key %s:%s" % (
|
||||
server_name, verify_key.alg, verify_key.version
|
||||
),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_server_verify_key(self, server_name, key_ids):
|
||||
"""Finds a verification key for the server with one of the key ids.
|
||||
Args:
|
||||
server_name (str): The name of the server to fetch a key for.
|
||||
keys_ids (list of str): The key_ids to check for.
|
||||
"""
|
||||
|
||||
# Check the datastore to see if we have one cached.
|
||||
cached = yield self.store.get_server_verify_keys(server_name, key_ids)
|
||||
|
||||
if cached:
|
||||
defer.returnValue(cached[0])
|
||||
return
|
||||
|
||||
# Try to fetch the key from the remote server.
|
||||
# TODO(markjh): Ratelimit requests to a given server.
|
||||
|
||||
(response, tls_certificate) = yield fetch_server_key(
|
||||
server_name, self.hs.tls_context_factory
|
||||
)
|
||||
|
||||
# Check the response.
|
||||
|
||||
x509_certificate_bytes = crypto.dump_certificate(
|
||||
crypto.FILETYPE_ASN1, tls_certificate
|
||||
)
|
||||
|
||||
if ("signatures" not in response
|
||||
or server_name not in response["signatures"]):
|
||||
raise ValueError("Key response not signed by remote server")
|
||||
|
||||
if "tls_certificate" not in response:
|
||||
raise ValueError("Key response missing TLS certificate")
|
||||
|
||||
tls_certificate_b64 = response["tls_certificate"]
|
||||
|
||||
if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
|
||||
raise ValueError("TLS certificate doesn't match")
|
||||
|
||||
verify_keys = {}
|
||||
for key_id, key_base64 in response["verify_keys"].items():
|
||||
if is_signing_algorithm_supported(key_id):
|
||||
key_bytes = decode_base64(key_base64)
|
||||
verify_key = decode_verify_key_bytes(key_id, key_bytes)
|
||||
verify_keys[key_id] = verify_key
|
||||
|
||||
for key_id in response["signatures"][server_name]:
|
||||
if key_id not in response["verify_keys"]:
|
||||
raise ValueError(
|
||||
"Key response must include verification keys for all"
|
||||
" signatures"
|
||||
)
|
||||
if key_id in verify_keys:
|
||||
verify_signed_json(
|
||||
response,
|
||||
server_name,
|
||||
verify_keys[key_id]
|
||||
)
|
||||
|
||||
# Cache the result in the datastore.
|
||||
|
||||
time_now_ms = self.clock.time_msec()
|
||||
|
||||
yield self.store.store_server_certificate(
|
||||
server_name,
|
||||
server_name,
|
||||
time_now_ms,
|
||||
tls_certificate,
|
||||
)
|
||||
|
||||
for key_id, key in verify_keys.items():
|
||||
yield self.store.store_server_verify_key(
|
||||
server_name, server_name, time_now_ms, key
|
||||
)
|
||||
|
||||
for key_id in key_ids:
|
||||
if key_id in verify_keys:
|
||||
defer.returnValue(verify_keys[key_id])
|
||||
return
|
||||
|
||||
raise ValueError("No verification key found for given key ids")
|
||||
@@ -1,111 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# 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 reactor, ssl
|
||||
from twisted.web import server
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.python.log import PythonLoggingObserver
|
||||
|
||||
from synapse.crypto.resource.key import LocalKey
|
||||
from synapse.crypto.config import load_config
|
||||
|
||||
from syutil.base64util import decode_base64
|
||||
|
||||
from OpenSSL import crypto, SSL
|
||||
|
||||
import logging
|
||||
import nacl.signing
|
||||
import sys
|
||||
|
||||
|
||||
class KeyServerSSLContextFactory(ssl.ContextFactory):
|
||||
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
||||
connections and to make connections to remote servers."""
|
||||
|
||||
def __init__(self, key_server):
|
||||
self._context = SSL.Context(SSL.SSLv23_METHOD)
|
||||
self.configure_context(self._context, key_server)
|
||||
|
||||
@staticmethod
|
||||
def configure_context(context, key_server):
|
||||
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||
context.use_certificate(key_server.tls_certificate)
|
||||
context.use_privatekey(key_server.tls_private_key)
|
||||
context.load_tmp_dh(key_server.tls_dh_params_path)
|
||||
context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
|
||||
|
||||
def getContext(self):
|
||||
return self._context
|
||||
|
||||
|
||||
class KeyServer(object):
|
||||
"""An HTTPS server serving LocalKey and RemoteKey resources."""
|
||||
|
||||
def __init__(self, server_name, tls_certificate_path, tls_private_key_path,
|
||||
tls_dh_params_path, signing_key_path, bind_host, bind_port):
|
||||
self.server_name = server_name
|
||||
self.tls_certificate = self.read_tls_certificate(tls_certificate_path)
|
||||
self.tls_private_key = self.read_tls_private_key(tls_private_key_path)
|
||||
self.tls_dh_params_path = tls_dh_params_path
|
||||
self.signing_key = self.read_signing_key(signing_key_path)
|
||||
self.bind_host = bind_host
|
||||
self.bind_port = int(bind_port)
|
||||
self.ssl_context_factory = KeyServerSSLContextFactory(self)
|
||||
|
||||
@staticmethod
|
||||
def read_tls_certificate(cert_path):
|
||||
with open(cert_path) as cert_file:
|
||||
cert_pem = cert_file.read()
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
||||
|
||||
@staticmethod
|
||||
def read_tls_private_key(private_key_path):
|
||||
with open(private_key_path) as private_key_file:
|
||||
private_key_pem = private_key_file.read()
|
||||
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
||||
|
||||
@staticmethod
|
||||
def read_signing_key(signing_key_path):
|
||||
with open(signing_key_path) as signing_key_file:
|
||||
signing_key_b64 = signing_key_file.read()
|
||||
signing_key_bytes = decode_base64(signing_key_b64)
|
||||
return nacl.signing.SigningKey(signing_key_bytes)
|
||||
|
||||
def run(self):
|
||||
root = Resource()
|
||||
root.putChild("key", LocalKey(self))
|
||||
site = server.Site(root)
|
||||
reactor.listenSSL(
|
||||
self.bind_port,
|
||||
site,
|
||||
self.ssl_context_factory,
|
||||
interface=self.bind_host
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
observer = PythonLoggingObserver()
|
||||
observer.start()
|
||||
|
||||
reactor.run()
|
||||
|
||||
|
||||
def main():
|
||||
key_server = KeyServer(**load_config(__doc__, sys.argv[1:]))
|
||||
key_server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,161 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# 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.web.resource import Resource
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
from twisted.internet import defer
|
||||
from synapse.http.server import respond_with_json_bytes
|
||||
from synapse.crypto.keyclient import fetch_server_key
|
||||
from syutil.crypto.jsonsign import sign_json, verify_signed_json
|
||||
from syutil.base64util import encode_base64, decode_base64
|
||||
from syutil.jsonutil import encode_canonical_json
|
||||
from OpenSSL import crypto
|
||||
from nacl.signing import VerifyKey
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalKey(Resource):
|
||||
"""HTTP resource containing encoding the TLS X.509 certificate and NACL
|
||||
signature verification keys for this server::
|
||||
|
||||
GET /key HTTP/1.1
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
{
|
||||
"server_name": "this.server.example.com"
|
||||
"signature_verify_key": # base64 encoded NACL verification key.
|
||||
"tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
|
||||
"signatures": {
|
||||
"this.server.example.com": # NACL signature for this server.
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, key_server):
|
||||
self.key_server = key_server
|
||||
self.response_body = encode_canonical_json(
|
||||
self.response_json_object(key_server)
|
||||
)
|
||||
Resource.__init__(self)
|
||||
|
||||
@staticmethod
|
||||
def response_json_object(key_server):
|
||||
verify_key_bytes = key_server.signing_key.verify_key.encode()
|
||||
x509_certificate_bytes = crypto.dump_certificate(
|
||||
crypto.FILETYPE_ASN1,
|
||||
key_server.tls_certificate
|
||||
)
|
||||
json_object = {
|
||||
u"server_name": key_server.server_name,
|
||||
u"signature_verify_key": encode_base64(verify_key_bytes),
|
||||
u"tls_certificate": encode_base64(x509_certificate_bytes)
|
||||
}
|
||||
signed_json = sign_json(
|
||||
json_object,
|
||||
key_server.server_name,
|
||||
key_server.signing_key
|
||||
)
|
||||
return signed_json
|
||||
|
||||
def getChild(self, name, request):
|
||||
logger.info("getChild %s %s", name, request)
|
||||
if name == '':
|
||||
return self
|
||||
else:
|
||||
return RemoteKey(name, self.key_server)
|
||||
|
||||
def render_GET(self, request):
|
||||
return respond_with_json_bytes(request, 200, self.response_body)
|
||||
|
||||
|
||||
class RemoteKey(Resource):
|
||||
"""HTTP resource for retreiving the TLS certificate and NACL signature
|
||||
verification keys for a another server. Checks that the reported X.509 TLS
|
||||
certificate matches the one used in the HTTPS connection. Checks that the
|
||||
NACL signature for the remote server is valid. Returns JSON signed by both
|
||||
the remote server and by this server.
|
||||
|
||||
GET /key/remote.server.example.com HTTP/1.1
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
{
|
||||
"server_name": "remote.server.example.com"
|
||||
"signature_verify_key": # base64 encoded NACL verification key.
|
||||
"tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
|
||||
"signatures": {
|
||||
"remote.server.example.com": # NACL signature for remote server.
|
||||
"this.server.example.com": # NACL signature for this server.
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, server_name, key_server):
|
||||
self.server_name = server_name
|
||||
self.key_server = key_server
|
||||
Resource.__init__(self)
|
||||
|
||||
def render_GET(self, request):
|
||||
self._async_render_GET(request)
|
||||
return NOT_DONE_YET
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _async_render_GET(self, request):
|
||||
try:
|
||||
server_keys, certificate = yield fetch_server_key(
|
||||
self.server_name,
|
||||
self.key_server.ssl_context_factory
|
||||
)
|
||||
|
||||
resp_server_name = server_keys[u"server_name"]
|
||||
verify_key_b64 = server_keys[u"signature_verify_key"]
|
||||
tls_certificate_b64 = server_keys[u"tls_certificate"]
|
||||
verify_key = VerifyKey(decode_base64(verify_key_b64))
|
||||
|
||||
if resp_server_name != self.server_name:
|
||||
raise ValueError("Wrong server name '%s' != '%s'" %
|
||||
(resp_server_name, self.server_name))
|
||||
|
||||
x509_certificate_bytes = crypto.dump_certificate(
|
||||
crypto.FILETYPE_ASN1,
|
||||
certificate
|
||||
)
|
||||
|
||||
if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
|
||||
raise ValueError("TLS certificate doesn't match")
|
||||
|
||||
verify_signed_json(server_keys, self.server_name, verify_key)
|
||||
|
||||
signed_json = sign_json(
|
||||
server_keys,
|
||||
self.key_server.server_name,
|
||||
self.key_server.signing_key
|
||||
)
|
||||
|
||||
json_bytes = encode_canonical_json(signed_json)
|
||||
respond_with_json_bytes(request, 200, json_bytes)
|
||||
|
||||
except Exception as e:
|
||||
json_bytes = encode_canonical_json({
|
||||
u"error": {u"code": 502, u"message": e.message}
|
||||
})
|
||||
respond_with_json_bytes(request, 502, json_bytes)
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ from .transport import TransportLayer
|
||||
|
||||
def initialize_http_replication(homeserver):
|
||||
transport = TransportLayer(
|
||||
homeserver,
|
||||
homeserver.hostname,
|
||||
server=homeserver.get_resource_for_federation(),
|
||||
client=homeserver.get_http_client()
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# 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 .units import Pdu
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
def decode_event_id(event_id, server_name):
|
||||
parts = event_id.split("@")
|
||||
if len(parts) < 2:
|
||||
return (event_id, server_name)
|
||||
else:
|
||||
return (parts[0], "".join(parts[1:]))
|
||||
|
||||
|
||||
def encode_event_id(pdu_id, origin):
|
||||
return "%s@%s" % (pdu_id, origin)
|
||||
|
||||
|
||||
class PduCodec(object):
|
||||
|
||||
def __init__(self, hs):
|
||||
self.server_name = hs.hostname
|
||||
self.event_factory = hs.get_event_factory()
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
def event_from_pdu(self, pdu):
|
||||
kwargs = {}
|
||||
|
||||
kwargs["event_id"] = encode_event_id(pdu.pdu_id, pdu.origin)
|
||||
kwargs["room_id"] = pdu.context
|
||||
kwargs["etype"] = pdu.pdu_type
|
||||
kwargs["prev_events"] = [
|
||||
encode_event_id(p[0], p[1]) for p in pdu.prev_pdus
|
||||
]
|
||||
|
||||
if hasattr(pdu, "prev_state_id") and hasattr(pdu, "prev_state_origin"):
|
||||
kwargs["prev_state"] = encode_event_id(
|
||||
pdu.prev_state_id, pdu.prev_state_origin
|
||||
)
|
||||
|
||||
kwargs.update({
|
||||
k: v
|
||||
for k, v in pdu.get_full_dict().items()
|
||||
if k not in [
|
||||
"pdu_id",
|
||||
"context",
|
||||
"pdu_type",
|
||||
"prev_pdus",
|
||||
"prev_state_id",
|
||||
"prev_state_origin",
|
||||
]
|
||||
})
|
||||
|
||||
return self.event_factory.create_event(**kwargs)
|
||||
|
||||
def pdu_from_event(self, event):
|
||||
d = event.get_full_dict()
|
||||
|
||||
d["pdu_id"], d["origin"] = decode_event_id(
|
||||
event.event_id, self.server_name
|
||||
)
|
||||
d["context"] = event.room_id
|
||||
d["pdu_type"] = event.type
|
||||
|
||||
if hasattr(event, "prev_events"):
|
||||
d["prev_pdus"] = [
|
||||
decode_event_id(e, self.server_name)
|
||||
for e in event.prev_events
|
||||
]
|
||||
|
||||
if hasattr(event, "prev_state"):
|
||||
d["prev_state_id"], d["prev_state_origin"] = (
|
||||
decode_event_id(event.prev_state, self.server_name)
|
||||
)
|
||||
|
||||
if hasattr(event, "state_key"):
|
||||
d["is_state"] = True
|
||||
|
||||
kwargs = copy.deepcopy(event.unrecognized_keys)
|
||||
kwargs.update({
|
||||
k: v for k, v in d.items()
|
||||
if k not in ["event_id", "room_id", "type", "prev_events"]
|
||||
})
|
||||
|
||||
if "ts" not in kwargs:
|
||||
kwargs["ts"] = int(self.clock.time_msec())
|
||||
|
||||
return Pdu(**kwargs)
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -21,8 +21,6 @@ These actions are mostly only used by the :py:mod:`.replication` module.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from .units import Pdu
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import json
|
||||
@@ -32,76 +30,6 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PduActions(object):
|
||||
""" Defines persistence actions that relate to handling PDUs.
|
||||
"""
|
||||
|
||||
def __init__(self, datastore):
|
||||
self.store = datastore
|
||||
|
||||
@log_function
|
||||
def mark_as_processed(self, pdu):
|
||||
""" Persist the fact that we have fully processed the given `Pdu`
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
return self.store.mark_pdu_as_processed(pdu.pdu_id, pdu.origin)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def after_transaction(self, transaction_id, destination, origin):
|
||||
""" Returns all `Pdu`s that we sent to the given remote home server
|
||||
after a given transaction id.
|
||||
|
||||
Returns:
|
||||
Deferred: Results in a list of `Pdu`s
|
||||
"""
|
||||
results = yield self.store.get_pdus_after_transaction(
|
||||
transaction_id,
|
||||
destination
|
||||
)
|
||||
|
||||
defer.returnValue([Pdu.from_pdu_tuple(p) for p in results])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def get_all_pdus_from_context(self, context):
|
||||
results = yield self.store.get_all_pdus_from_context(context)
|
||||
defer.returnValue([Pdu.from_pdu_tuple(p) for p in results])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def backfill(self, context, pdu_list, limit):
|
||||
""" For a given list of PDU id and origins return the proceeding
|
||||
`limit` `Pdu`s in the given `context`.
|
||||
|
||||
Returns:
|
||||
Deferred: Results in a list of `Pdu`s.
|
||||
"""
|
||||
results = yield self.store.get_backfill(
|
||||
context, pdu_list, limit
|
||||
)
|
||||
|
||||
defer.returnValue([Pdu.from_pdu_tuple(p) for p in results])
|
||||
|
||||
@log_function
|
||||
def is_new(self, pdu):
|
||||
""" When we receive a `Pdu` from a remote home server, we want to
|
||||
figure out whether it is `new`, i.e. it is not some historic PDU that
|
||||
we haven't seen simply because we haven't backfilled back that far.
|
||||
|
||||
Returns:
|
||||
Deferred: Results in a `bool`
|
||||
"""
|
||||
return self.store.is_pdu_new(
|
||||
pdu_id=pdu.pdu_id,
|
||||
origin=pdu.origin,
|
||||
context=pdu.context,
|
||||
depth=pdu.depth
|
||||
)
|
||||
|
||||
|
||||
class TransactionActions(object):
|
||||
""" Defines persistence actions that relate to handling Transactions.
|
||||
"""
|
||||
@@ -157,8 +85,7 @@ class TransactionActions(object):
|
||||
transaction.prev_ids = yield self.store.prep_send_transaction(
|
||||
transaction.transaction_id,
|
||||
transaction.destination,
|
||||
transaction.ts,
|
||||
[(p["pdu_id"], p["origin"]) for p in transaction.pdus]
|
||||
transaction.origin_server_ts,
|
||||
)
|
||||
|
||||
@log_function
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
# Copyright 2014 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -19,11 +19,12 @@ a given transport.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from .units import Transaction, Pdu, Edu
|
||||
from .units import Transaction, Edu
|
||||
|
||||
from .persistence import PduActions, TransactionActions
|
||||
from .persistence import TransactionActions
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
from synapse.util.logcontext import PreserveLoggingContext
|
||||
|
||||
import logging
|
||||
|
||||
@@ -57,7 +58,7 @@ class ReplicationLayer(object):
|
||||
self.transport_layer.register_request_handler(self)
|
||||
|
||||
self.store = hs.get_datastore()
|
||||
self.pdu_actions = PduActions(self.store)
|
||||
# self.pdu_actions = PduActions(self.store)
|
||||
self.transaction_actions = TransactionActions(self.store)
|
||||
|
||||
self._transaction_queue = _TransactionQueue(
|
||||
@@ -72,6 +73,8 @@ class ReplicationLayer(object):
|
||||
|
||||
self._clock = hs.get_clock()
|
||||
|
||||
self.event_factory = hs.get_event_factory()
|
||||
|
||||
def set_handler(self, handler):
|
||||
"""Sets the handler that the replication layer will use to communicate
|
||||
receipt of new PDUs from other home servers. The required methods are
|
||||
@@ -81,7 +84,7 @@ class ReplicationLayer(object):
|
||||
|
||||
def register_edu_handler(self, edu_type, handler):
|
||||
if edu_type in self.edu_handlers:
|
||||
raise KeyError("Already have an EDU handler for %s" % (edu_type))
|
||||
raise KeyError("Already have an EDU handler for %s" % (edu_type,))
|
||||
|
||||
self.edu_handlers[edu_type] = handler
|
||||
|
||||
@@ -102,24 +105,17 @@ class ReplicationLayer(object):
|
||||
object to encode as JSON.
|
||||
"""
|
||||
if query_type in self.query_handlers:
|
||||
raise KeyError("Already have a Query handler for %s" % (query_type))
|
||||
raise KeyError(
|
||||
"Already have a Query handler for %s" % (query_type,)
|
||||
)
|
||||
|
||||
self.query_handlers[query_type] = handler
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def send_pdu(self, pdu):
|
||||
"""Informs the replication layer about a new PDU generated within the
|
||||
home server that should be transmitted to others.
|
||||
|
||||
This will fill out various attributes on the PDU object, e.g. the
|
||||
`prev_pdus` key.
|
||||
|
||||
*Note:* The home server should always call `send_pdu` even if it knows
|
||||
that it does not need to be replicated to other home servers. This is
|
||||
in case e.g. someone else joins via a remote home server and then
|
||||
backfills.
|
||||
|
||||
TODO: Figure out when we should actually resolve the deferred.
|
||||
|
||||
Args:
|
||||
@@ -132,18 +128,15 @@ class ReplicationLayer(object):
|
||||
order = self._order
|
||||
self._order += 1
|
||||
|
||||
logger.debug("[%s] Persisting PDU", pdu.pdu_id)
|
||||
|
||||
# Save *before* trying to send
|
||||
yield self.store.persist_event(pdu=pdu)
|
||||
|
||||
logger.debug("[%s] Persisted PDU", pdu.pdu_id)
|
||||
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.pdu_id)
|
||||
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.event_id)
|
||||
|
||||
# TODO, add errback, etc.
|
||||
self._transaction_queue.enqueue_pdu(pdu, order)
|
||||
|
||||
logger.debug("[%s] transaction_layer.enqueue_pdu... done", pdu.pdu_id)
|
||||
logger.debug(
|
||||
"[%s] transaction_layer.enqueue_pdu... done",
|
||||
pdu.event_id
|
||||
)
|
||||
|
||||
@log_function
|
||||
def send_edu(self, destination, edu_type, content):
|
||||
@@ -159,7 +152,13 @@ class ReplicationLayer(object):
|
||||
return defer.succeed(None)
|
||||
|
||||
@log_function
|
||||
def make_query(self, destination, query_type, args):
|
||||
def send_failure(self, failure, destination):
|
||||
self._transaction_queue.enqueue_failure(failure, destination)
|
||||
return defer.succeed(None)
|
||||
|
||||
@log_function
|
||||
def make_query(self, destination, query_type, args,
|
||||
retry_on_dns_fail=True):
|
||||
"""Sends a federation Query to a remote homeserver of the given type
|
||||
and arguments.
|
||||
|
||||
@@ -174,11 +173,13 @@ class ReplicationLayer(object):
|
||||
a Deferred which will eventually yield a JSON object from the
|
||||
response
|
||||
"""
|
||||
return self.transport_layer.make_query(destination, query_type, args)
|
||||
return self.transport_layer.make_query(
|
||||
destination, query_type, args, retry_on_dns_fail=retry_on_dns_fail
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def backfill(self, dest, context, limit):
|
||||
def backfill(self, dest, context, limit, extremities):
|
||||
"""Requests some more historic PDUs for the given context from the
|
||||
given destination server.
|
||||
|
||||
@@ -186,12 +187,12 @@ class ReplicationLayer(object):
|
||||
dest (str): The remote home server to ask.
|
||||
context (str): The context to backfill.
|
||||
limit (int): The maximum number of PDUs to return.
|
||||
extremities (list): List of PDU id and origins of the first pdus
|
||||
we have seen from the context
|
||||
|
||||
Returns:
|
||||
Deferred: Results in the received PDUs.
|
||||
"""
|
||||
extremities = yield self.store.get_oldest_pdus_in_context(context)
|
||||
|
||||
logger.debug("backfill extrem=%s", extremities)
|
||||
|
||||
# If there are no extremeties then we've (probably) reached the start.
|
||||
@@ -205,15 +206,18 @@ class ReplicationLayer(object):
|
||||
|
||||
transaction = Transaction(**transaction_data)
|
||||
|
||||
pdus = [Pdu(outlier=False, **p) for p in transaction.pdus]
|
||||
pdus = [
|
||||
self.event_from_pdu_json(p, outlier=False)
|
||||
for p in transaction.pdus
|
||||
]
|
||||
for pdu in pdus:
|
||||
yield self._handle_new_pdu(pdu, backfilled=True)
|
||||
yield self._handle_new_pdu(dest, pdu, backfilled=True)
|
||||
|
||||
defer.returnValue(pdus)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def get_pdu(self, destination, pdu_origin, pdu_id, outlier=False):
|
||||
def get_pdu(self, destination, event_id, outlier=False):
|
||||
"""Requests the PDU with given origin and ID from the remote home
|
||||
server.
|
||||
|
||||
@@ -222,7 +226,7 @@ class ReplicationLayer(object):
|
||||
Args:
|
||||
destination (str): Which home server to query
|
||||
pdu_origin (str): The home server that originally sent the pdu.
|
||||
pdu_id (str)
|
||||
event_id (str)
|
||||
outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if
|
||||
it's from an arbitary point in the context as opposed to part
|
||||
of the current block of PDUs. Defaults to `False`
|
||||
@@ -231,23 +235,27 @@ class ReplicationLayer(object):
|
||||
Deferred: Results in the requested PDU.
|
||||
"""
|
||||
|
||||
transaction_data = yield self.transport_layer.get_pdu(
|
||||
destination, pdu_origin, pdu_id)
|
||||
transaction_data = yield self.transport_layer.get_event(
|
||||
destination, event_id
|
||||
)
|
||||
|
||||
transaction = Transaction(**transaction_data)
|
||||
|
||||
pdu_list = [Pdu(outlier=outlier, **p) for p in transaction.pdus]
|
||||
pdu_list = [
|
||||
self.event_from_pdu_json(p, outlier=outlier)
|
||||
for p in transaction.pdus
|
||||
]
|
||||
|
||||
pdu = None
|
||||
if pdu_list:
|
||||
pdu = pdu_list[0]
|
||||
yield self._handle_new_pdu(pdu)
|
||||
yield self._handle_new_pdu(destination, pdu)
|
||||
|
||||
defer.returnValue(pdu)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def get_state_for_context(self, destination, context):
|
||||
def get_state_for_context(self, destination, context, event_id=None):
|
||||
"""Requests all of the `current` state PDUs for a given context from
|
||||
a remote home server.
|
||||
|
||||
@@ -260,29 +268,41 @@ class ReplicationLayer(object):
|
||||
"""
|
||||
|
||||
transaction_data = yield self.transport_layer.get_context_state(
|
||||
destination, context)
|
||||
destination,
|
||||
context,
|
||||
event_id=event_id,
|
||||
)
|
||||
|
||||
transaction = Transaction(**transaction_data)
|
||||
|
||||
pdus = [Pdu(outlier=True, **p) for p in transaction.pdus]
|
||||
for pdu in pdus:
|
||||
yield self._handle_new_pdu(pdu)
|
||||
pdus = [
|
||||
self.event_from_pdu_json(p, outlier=True)
|
||||
for p in transaction.pdus
|
||||
]
|
||||
|
||||
defer.returnValue(pdus)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def on_context_pdus_request(self, context):
|
||||
pdus = yield self.pdu_actions.get_all_pdus_from_context(
|
||||
context
|
||||
def get_event_auth(self, destination, context, event_id):
|
||||
res = yield self.transport_layer.get_event_auth(
|
||||
destination, context, event_id,
|
||||
)
|
||||
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
||||
|
||||
auth_chain = [
|
||||
self.event_from_pdu_json(p, outlier=True)
|
||||
for p in res["auth_chain"]
|
||||
]
|
||||
|
||||
auth_chain.sort(key=lambda e: e.depth)
|
||||
|
||||
defer.returnValue(auth_chain)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def on_backfill_request(self, context, versions, limit):
|
||||
|
||||
pdus = yield self.pdu_actions.backfill(context, versions, limit)
|
||||
def on_backfill_request(self, origin, context, versions, limit):
|
||||
pdus = yield self.handler.on_backfill_request(
|
||||
origin, context, versions, limit
|
||||
)
|
||||
|
||||
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
||||
|
||||
@@ -291,6 +311,19 @@ class ReplicationLayer(object):
|
||||
def on_incoming_transaction(self, transaction_data):
|
||||
transaction = Transaction(**transaction_data)
|
||||
|
||||
for p in transaction.pdus:
|
||||
if "unsigned" in p:
|
||||
unsigned = p["unsigned"]
|
||||
if "age" in unsigned:
|
||||
p["age"] = unsigned["age"]
|
||||
if "age" in p:
|
||||
p["age_ts"] = int(self._clock.time_msec()) - int(p["age"])
|
||||
del p["age"]
|
||||
|
||||
pdu_list = [
|
||||
self.event_from_pdu_json(p) for p in transaction.pdus
|
||||
]
|
||||
|
||||
logger.debug("[%s] Got transaction", transaction.transaction_id)
|
||||
|
||||
response = yield self.transaction_actions.have_responded(transaction)
|
||||
@@ -303,17 +336,20 @@ class ReplicationLayer(object):
|
||||
|
||||
logger.debug("[%s] Transacition is new", transaction.transaction_id)
|
||||
|
||||
pdu_list = [Pdu(**p) for p in transaction.pdus]
|
||||
with PreserveLoggingContext():
|
||||
dl = []
|
||||
for pdu in pdu_list:
|
||||
dl.append(self._handle_new_pdu(transaction.origin, pdu))
|
||||
|
||||
dl = []
|
||||
for pdu in pdu_list:
|
||||
dl.append(self._handle_new_pdu(pdu))
|
||||
if hasattr(transaction, "edus"):
|
||||
for edu in [Edu(**x) for x in transaction.edus]:
|
||||
self.received_edu(
|
||||
transaction.origin,
|
||||
edu.edu_type,
|
||||
edu.content
|
||||
)
|
||||
|
||||
if hasattr(transaction, "edus"):
|
||||
for edu in [Edu(**x) for x in transaction.edus]:
|
||||
self.received_edu(edu.origin, edu.edu_type, edu.content)
|
||||
|
||||
results = yield defer.DeferredList(dl)
|
||||
results = yield defer.DeferredList(dl)
|
||||
|
||||
ret = []
|
||||
for r in results:
|
||||
@@ -339,20 +375,22 @@ class ReplicationLayer(object):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def on_context_state_request(self, context):
|
||||
results = yield self.store.get_current_state_for_context(
|
||||
context
|
||||
)
|
||||
def on_context_state_request(self, origin, context, event_id):
|
||||
if event_id:
|
||||
pdus = yield self.handler.get_state_for_pdu(
|
||||
origin,
|
||||
context,
|
||||
event_id,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("Specify an event")
|
||||
|
||||
logger.debug("Context returning %d results", len(results))
|
||||
|
||||
pdus = [Pdu.from_pdu_tuple(p) for p in results]
|
||||
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def on_pdu_request(self, pdu_origin, pdu_id):
|
||||
pdu = yield self._get_persisted_pdu(pdu_id, pdu_origin)
|
||||
def on_pdu_request(self, origin, event_id):
|
||||
pdu = yield self._get_persisted_pdu(origin, event_id)
|
||||
|
||||
if pdu:
|
||||
defer.returnValue(
|
||||
@@ -364,20 +402,7 @@ class ReplicationLayer(object):
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def on_pull_request(self, origin, versions):
|
||||
transaction_id = max([int(v) for v in versions])
|
||||
|
||||
response = yield self.pdu_actions.after_transaction(
|
||||
transaction_id,
|
||||
origin,
|
||||
self.server_name
|
||||
)
|
||||
|
||||
if not response:
|
||||
response = []
|
||||
|
||||
defer.returnValue(
|
||||
(200, self._transaction_from_pdus(response).get_dict())
|
||||
)
|
||||
raise NotImplementedError("Pull transacions not implemented")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_query_request(self, query_type, args):
|
||||
@@ -385,90 +410,266 @@ class ReplicationLayer(object):
|
||||
response = yield self.query_handlers[query_type](args)
|
||||
defer.returnValue((200, response))
|
||||
else:
|
||||
defer.returnValue((404, "No handler for Query type '%s'"
|
||||
% (query_type)
|
||||
))
|
||||
defer.returnValue(
|
||||
(404, "No handler for Query type '%s'" % (query_type, ))
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_make_join_request(self, context, user_id):
|
||||
pdu = yield self.handler.on_make_join_request(context, user_id)
|
||||
time_now = self._clock.time_msec()
|
||||
defer.returnValue({
|
||||
"event": pdu.get_pdu_json(time_now),
|
||||
})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_invite_request(self, origin, content):
|
||||
pdu = self.event_from_pdu_json(content)
|
||||
ret_pdu = yield self.handler.on_invite_request(origin, pdu)
|
||||
time_now = self._clock.time_msec()
|
||||
defer.returnValue(
|
||||
(
|
||||
200,
|
||||
{
|
||||
"event": ret_pdu.get_pdu_json(time_now),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_send_join_request(self, origin, content):
|
||||
pdu = self.event_from_pdu_json(content)
|
||||
res_pdus = yield self.handler.on_send_join_request(origin, pdu)
|
||||
time_now = self._clock.time_msec()
|
||||
defer.returnValue((200, {
|
||||
"state": [p.get_pdu_json(time_now) for p in res_pdus["state"]],
|
||||
"auth_chain": [
|
||||
p.get_pdu_json(time_now) for p in res_pdus["auth_chain"]
|
||||
],
|
||||
}))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_event_auth(self, origin, context, event_id):
|
||||
time_now = self._clock.time_msec()
|
||||
auth_pdus = yield self.handler.on_event_auth(event_id)
|
||||
defer.returnValue(
|
||||
(
|
||||
200,
|
||||
{
|
||||
"auth_chain": [
|
||||
a.get_pdu_json(time_now) for a in auth_pdus
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def make_join(self, destination, context, user_id):
|
||||
ret = yield self.transport_layer.make_join(
|
||||
destination=destination,
|
||||
context=context,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
pdu_dict = ret["event"]
|
||||
|
||||
logger.debug("Got response to make_join: %s", pdu_dict)
|
||||
|
||||
defer.returnValue(self.event_from_pdu_json(pdu_dict))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_join(self, destination, pdu):
|
||||
time_now = self._clock.time_msec()
|
||||
_, content = yield self.transport_layer.send_join(
|
||||
destination,
|
||||
pdu.room_id,
|
||||
pdu.event_id,
|
||||
pdu.get_pdu_json(time_now),
|
||||
)
|
||||
|
||||
logger.debug("Got content: %s", content)
|
||||
|
||||
state = [
|
||||
self.event_from_pdu_json(p, outlier=True)
|
||||
for p in content.get("state", [])
|
||||
]
|
||||
|
||||
# FIXME: We probably want to do something with the auth_chain given
|
||||
# to us
|
||||
|
||||
auth_chain = [
|
||||
self.event_from_pdu_json(p, outlier=True)
|
||||
for p in content.get("auth_chain", [])
|
||||
]
|
||||
|
||||
auth_chain.sort(key=lambda e: e.depth)
|
||||
|
||||
defer.returnValue({
|
||||
"state": state,
|
||||
"auth_chain": auth_chain,
|
||||
})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_invite(self, destination, context, event_id, pdu):
|
||||
time_now = self._clock.time_msec()
|
||||
code, content = yield self.transport_layer.send_invite(
|
||||
destination=destination,
|
||||
context=context,
|
||||
event_id=event_id,
|
||||
content=pdu.get_pdu_json(time_now),
|
||||
)
|
||||
|
||||
pdu_dict = content["event"]
|
||||
|
||||
logger.debug("Got response to send_invite: %s", pdu_dict)
|
||||
|
||||
defer.returnValue(self.event_from_pdu_json(pdu_dict))
|
||||
|
||||
@log_function
|
||||
def _get_persisted_pdu(self, pdu_id, pdu_origin):
|
||||
def _get_persisted_pdu(self, origin, event_id, do_auth=True):
|
||||
""" Get a PDU from the database with given origin and id.
|
||||
|
||||
Returns:
|
||||
Deferred: Results in a `Pdu`.
|
||||
"""
|
||||
pdu_tuple = yield self.store.get_pdu(pdu_id, pdu_origin)
|
||||
|
||||
defer.returnValue(Pdu.from_pdu_tuple(pdu_tuple))
|
||||
return self.handler.get_persisted_pdu(
|
||||
origin, event_id, do_auth=do_auth
|
||||
)
|
||||
|
||||
def _transaction_from_pdus(self, pdu_list):
|
||||
"""Returns a new Transaction containing the given PDUs suitable for
|
||||
transmission.
|
||||
"""
|
||||
time_now = self._clock.time_msec()
|
||||
pdus = [p.get_pdu_json(time_now) for p in pdu_list]
|
||||
return Transaction(
|
||||
pdus=[p.get_dict() for p in pdu_list],
|
||||
origin=self.server_name,
|
||||
ts=int(self._clock.time_msec()),
|
||||
pdus=pdus,
|
||||
origin_server_ts=int(time_now),
|
||||
destination=None,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def _handle_new_pdu(self, pdu, backfilled=False):
|
||||
def _handle_new_pdu(self, origin, pdu, backfilled=False):
|
||||
# We reprocess pdus when we have seen them only as outliers
|
||||
existing = yield self._get_persisted_pdu(pdu.pdu_id, pdu.origin)
|
||||
existing = yield self._get_persisted_pdu(
|
||||
origin, pdu.event_id, do_auth=False
|
||||
)
|
||||
|
||||
if existing and (not existing.outlier or pdu.outlier):
|
||||
logger.debug("Already seen pdu %s %s", pdu.pdu_id, pdu.origin)
|
||||
logger.debug("Already seen pdu %s", pdu.event_id)
|
||||
defer.returnValue({})
|
||||
return
|
||||
|
||||
state = None
|
||||
|
||||
# We need to make sure we have all the auth events.
|
||||
# for e_id, _ in pdu.auth_events:
|
||||
# exists = yield self._get_persisted_pdu(
|
||||
# origin,
|
||||
# e_id,
|
||||
# do_auth=False
|
||||
# )
|
||||
#
|
||||
# if not exists:
|
||||
# try:
|
||||
# logger.debug(
|
||||
# "_handle_new_pdu fetch missing auth event %s from %s",
|
||||
# e_id,
|
||||
# origin,
|
||||
# )
|
||||
#
|
||||
# yield self.get_pdu(
|
||||
# origin,
|
||||
# event_id=e_id,
|
||||
# outlier=True,
|
||||
# )
|
||||
#
|
||||
# logger.debug("Processed pdu %s", e_id)
|
||||
# except:
|
||||
# logger.warn(
|
||||
# "Failed to get auth event %s from %s",
|
||||
# e_id,
|
||||
# origin
|
||||
# )
|
||||
|
||||
# Get missing pdus if necessary.
|
||||
is_new = yield self.pdu_actions.is_new(pdu)
|
||||
if is_new and not pdu.outlier:
|
||||
if not pdu.outlier:
|
||||
# We only backfill backwards to the min depth.
|
||||
min_depth = yield self.store.get_min_depth_for_context(pdu.context)
|
||||
min_depth = yield self.handler.get_min_depth_for_context(
|
||||
pdu.room_id
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"_handle_new_pdu min_depth for %s: %d",
|
||||
pdu.room_id, min_depth
|
||||
)
|
||||
|
||||
if min_depth and pdu.depth > min_depth:
|
||||
for pdu_id, origin in pdu.prev_pdus:
|
||||
exists = yield self._get_persisted_pdu(pdu_id, origin)
|
||||
for event_id, hashes in pdu.prev_events:
|
||||
exists = yield self._get_persisted_pdu(
|
||||
origin,
|
||||
event_id,
|
||||
do_auth=False
|
||||
)
|
||||
|
||||
if not exists:
|
||||
logger.debug("Requesting pdu %s %s", pdu_id, origin)
|
||||
logger.debug(
|
||||
"_handle_new_pdu requesting pdu %s",
|
||||
event_id
|
||||
)
|
||||
|
||||
try:
|
||||
yield self.get_pdu(
|
||||
pdu.origin,
|
||||
pdu_id=pdu_id,
|
||||
pdu_origin=origin
|
||||
origin,
|
||||
event_id=event_id,
|
||||
)
|
||||
logger.debug("Processed pdu %s %s", pdu_id, origin)
|
||||
logger.debug("Processed pdu %s", event_id)
|
||||
except:
|
||||
# TODO(erikj): Do some more intelligent retries.
|
||||
logger.exception("Failed to get PDU")
|
||||
|
||||
# Persist the Pdu, but don't mark it as processed yet.
|
||||
yield self.store.persist_event(pdu=pdu)
|
||||
else:
|
||||
# We need to get the state at this event, since we have reached
|
||||
# a backward extremity edge.
|
||||
logger.debug(
|
||||
"_handle_new_pdu getting state for %s",
|
||||
pdu.room_id
|
||||
)
|
||||
state = yield self.get_state_for_context(
|
||||
origin, pdu.room_id, pdu.event_id,
|
||||
)
|
||||
|
||||
if not backfilled:
|
||||
ret = yield self.handler.on_receive_pdu(pdu, backfilled=backfilled)
|
||||
ret = yield self.handler.on_receive_pdu(
|
||||
origin,
|
||||
pdu,
|
||||
backfilled=backfilled,
|
||||
state=state,
|
||||
)
|
||||
else:
|
||||
ret = None
|
||||
|
||||
yield self.pdu_actions.mark_as_processed(pdu)
|
||||
# yield self.pdu_actions.mark_as_processed(pdu)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
def __str__(self):
|
||||
return "<ReplicationLayer(%s)>" % self.server_name
|
||||
|
||||
|
||||
class ReplicationHandler(object):
|
||||
"""This defines the methods that the :py:class:`.ReplicationLayer` will
|
||||
use to communicate with the rest of the home server.
|
||||
"""
|
||||
def on_receive_pdu(self, pdu):
|
||||
raise NotImplementedError("on_receive_pdu")
|
||||
def event_from_pdu_json(self, pdu_json, outlier=False):
|
||||
#TODO: Check we have all the PDU keys here
|
||||
pdu_json.setdefault("hashes", {})
|
||||
pdu_json.setdefault("signatures", {})
|
||||
sender = pdu_json.pop("sender", None)
|
||||
if sender is not None:
|
||||
pdu_json["user_id"] = sender
|
||||
state_hash = pdu_json.get("unsigned", {}).pop("state_hash", None)
|
||||
if state_hash is not None:
|
||||
pdu_json["state_hash"] = state_hash
|
||||
return self.event_factory.create_event(
|
||||
pdu_json["type"], outlier=outlier, **pdu_json
|
||||
)
|
||||
|
||||
|
||||
class _TransactionQueue(object):
|
||||
@@ -479,7 +680,6 @@ class _TransactionQueue(object):
|
||||
"""
|
||||
|
||||
def __init__(self, hs, transaction_actions, transport_layer):
|
||||
|
||||
self.server_name = hs.hostname
|
||||
self.transaction_actions = transaction_actions
|
||||
self.transport_layer = transport_layer
|
||||
@@ -497,6 +697,9 @@ class _TransactionQueue(object):
|
||||
# destination -> list of tuple(edu, deferred)
|
||||
self.pending_edus_by_dest = {}
|
||||
|
||||
# destination -> list of tuple(failure, deferred)
|
||||
self.pending_failures_by_dest = {}
|
||||
|
||||
# HACK to get unique tx id
|
||||
self._next_txn_id = int(self._clock.time_msec())
|
||||
|
||||
@@ -525,7 +728,8 @@ class _TransactionQueue(object):
|
||||
(pdu, deferred, order)
|
||||
)
|
||||
|
||||
self._attempt_new_transaction(destination)
|
||||
with PreserveLoggingContext():
|
||||
self._attempt_new_transaction(destination)
|
||||
|
||||
deferreds.append(deferred)
|
||||
|
||||
@@ -545,10 +749,24 @@ class _TransactionQueue(object):
|
||||
deferred.errback(failure)
|
||||
else:
|
||||
logger.exception("Failed to send edu", failure)
|
||||
self._attempt_new_transaction(destination).addErrback(eb)
|
||||
|
||||
with PreserveLoggingContext():
|
||||
self._attempt_new_transaction(destination).addErrback(eb)
|
||||
|
||||
return deferred
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def enqueue_failure(self, failure, destination):
|
||||
deferred = defer.Deferred()
|
||||
|
||||
self.pending_failures_by_dest.setdefault(
|
||||
destination, []
|
||||
).append(
|
||||
(failure, deferred)
|
||||
)
|
||||
|
||||
yield deferred
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def _attempt_new_transaction(self, destination):
|
||||
@@ -558,8 +776,9 @@ class _TransactionQueue(object):
|
||||
# list of (pending_pdu, deferred, order)
|
||||
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
||||
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
||||
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
||||
|
||||
if not pending_pdus and not pending_edus:
|
||||
if not pending_pdus and not pending_edus and not pending_failures:
|
||||
return
|
||||
|
||||
logger.debug("TX [%s] Attempting new transaction", destination)
|
||||
@@ -569,7 +788,11 @@ class _TransactionQueue(object):
|
||||
|
||||
pdus = [x[0] for x in pending_pdus]
|
||||
edus = [x[0] for x in pending_edus]
|
||||
deferreds = [x[1] for x in pending_pdus + pending_edus]
|
||||
failures = [x[0].get_dict() for x in pending_failures]
|
||||
deferreds = [
|
||||
x[1]
|
||||
for x in pending_pdus + pending_edus + pending_failures
|
||||
]
|
||||
|
||||
try:
|
||||
self.pending_transactions[destination] = 1
|
||||
@@ -577,12 +800,13 @@ class _TransactionQueue(object):
|
||||
logger.debug("TX [%s] Persisting transaction...", destination)
|
||||
|
||||
transaction = Transaction.create_new(
|
||||
ts=self._clock.time_msec(),
|
||||
transaction_id=self._next_txn_id,
|
||||
origin_server_ts=int(self._clock.time_msec()),
|
||||
transaction_id=str(self._next_txn_id),
|
||||
origin=self.server_name,
|
||||
destination=destination,
|
||||
pdus=pdus,
|
||||
edus=edus,
|
||||
pdu_failures=failures,
|
||||
)
|
||||
|
||||
self._next_txn_id += 1
|
||||
@@ -593,8 +817,22 @@ class _TransactionQueue(object):
|
||||
logger.debug("TX [%s] Sending transaction...", destination)
|
||||
|
||||
# Actually send the transaction
|
||||
|
||||
# FIXME (erikj): This is a bit of a hack to make the Pdu age
|
||||
# keys work
|
||||
def json_data_cb():
|
||||
data = transaction.get_dict()
|
||||
now = int(self._clock.time_msec())
|
||||
if "pdus" in data:
|
||||
for p in data["pdus"]:
|
||||
if "age_ts" in p:
|
||||
unsigned = p.setdefault("unsigned", {})
|
||||
unsigned["age"] = now - int(p["age_ts"])
|
||||
del p["age_ts"]
|
||||
return data
|
||||
|
||||
code, response = yield self.transport_layer.send_transaction(
|
||||
transaction
|
||||
transaction, json_data_cb
|
||||
)
|
||||
|
||||
logger.debug("TX [%s] Sent transaction", destination)
|
||||
@@ -628,7 +866,6 @@ class _TransactionQueue(object):
|
||||
|
||||
for deferred in deferreds:
|
||||
deferred.errback(e)
|
||||
yield deferred
|
||||
|
||||
finally:
|
||||
# We want to be *very* sure we delete this after we stop processing
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user