mirror of
https://github.com/strukturag/nextcloud-spreed-signaling
synced 2026-03-14 22:45:44 +01:00
Compare commits
810 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbd910e489 |
||
|
|
aa7ca9d02f |
||
|
|
e7a8fb7aa9 |
||
|
|
d069a924fc |
||
|
|
9d2cda499e |
||
|
|
182a0b78e2 |
||
|
|
bd3c06c9eb |
||
|
|
2bf6d00a13 |
||
|
|
a6a1c35347 |
||
|
|
a9e58ec60a |
||
|
|
9c10675867 |
||
|
|
f195492f8e |
||
|
|
825d747f0a |
||
|
|
4d42c5e538 |
||
|
|
0616b9e0ce |
||
|
|
26f4a31871 |
||
|
|
eca3ee8bfb |
||
|
|
c7cb648b60 |
||
|
|
cdca1097d5 |
||
|
|
c0a056d3eb |
||
|
|
f3b2513ae4 |
||
|
|
10c1d25f20 |
||
|
|
a3afe9429d |
||
|
|
d97080c118 |
||
|
|
0ca5d7c3dc |
||
|
|
c6b8308259 |
||
|
|
ebd28a1ffe |
||
|
|
ddb0cdd72f |
||
|
|
9771976acf |
||
|
|
4e6bfc963c |
||
|
|
33a9c586c9 |
||
|
|
65ec614fcd |
||
|
|
bdb862c017 |
||
|
|
4a7eb3c2e4 |
||
|
|
d3d1644848 |
||
|
|
764f46e1a0 |
||
|
|
af250e4813 |
||
|
|
6e9d948a8c |
||
|
|
4468b9234f |
||
|
|
8f3e67052d |
||
|
|
6da684846d |
||
|
|
812b97e36a |
||
|
|
988ba0e8fd |
||
|
|
e78e914a68 |
||
|
|
3a3ce9f3f8 |
||
|
|
81fe82eb98 |
||
|
|
3fcd93a0b5 |
||
|
|
fd61bdce81 |
||
|
|
fa18ce95dd | ||
|
|
12dfad5c3a |
||
|
|
543fa397e6 |
||
|
|
bd9a7b2ba9 | ||
|
|
194494cd2c |
||
|
|
5051a6b194 |
||
|
|
750656ea89 |
||
|
|
9d45e78809 |
||
|
|
d0cfa058d8 |
||
|
|
50bcd61da0 |
||
|
|
8d0cbfa88a |
||
|
|
5da3d601fe |
||
|
|
d47c280d31 |
||
|
|
de03ae2514 |
||
|
|
a5f70b6e47 |
||
|
|
e4013b9f14 |
||
|
|
bd8c758847 |
||
|
|
6caac6fbca |
||
|
|
95c66f9d82 |
||
|
|
15d6e516bd |
||
|
|
083858294a |
||
|
|
9b062a994a |
||
|
|
ba482a544b |
||
|
|
f9f2347d11 |
||
|
|
73af7bb367 |
||
|
|
9b6616257b |
||
|
|
383494585d |
||
|
|
43e5108373 |
||
|
|
af732300e3 |
||
|
|
e4dd763d02 |
||
|
|
a0d3dd100f |
||
|
|
33a9c1bde2 |
||
|
|
4a7ecc3ac5 |
||
|
|
0b46f9d17c |
||
|
|
b004fef9ec |
||
|
|
7dbce454e6 |
||
|
|
cc87670153 |
||
|
|
ecac46bf5f |
||
|
|
a92819f3b2 |
||
|
|
5ddfe3dc50 |
||
|
|
9c199a044b |
||
|
|
f921e4a2c0 |
||
|
|
99762a3ca9 |
||
|
|
c2c9d0725f |
||
|
|
15f2d3cd5c |
||
|
|
806ef1f564 |
||
|
|
9cc07c8a65 |
||
|
|
20c0fe086b |
||
|
|
2a3c1660e3 |
||
|
|
f1dc9d6c5f |
||
|
|
a5b583910a |
||
|
|
10175d6cca |
||
|
|
daa542f80a |
||
|
|
9a1e4121d3 |
||
|
|
5fc709434b |
||
|
|
1d4ffd33fc |
||
|
|
d129c5c55b |
||
|
|
b733b6fa60 |
||
|
|
710264e366 |
||
|
|
cde3ee53a9 |
||
|
|
cb6df05cd3 |
||
|
|
15d734d77b |
||
|
|
77f0672682 |
||
|
|
eafa39a1c5 |
||
|
|
043c854cd2 |
||
|
|
d80143af4c |
||
|
|
66e2d73ee5 |
||
|
|
7ea4691404 |
||
|
|
3b667ffcf6 |
||
|
|
4c4abb16ce |
||
|
|
b11d80c0f3 |
||
|
|
9891003304 |
||
|
|
9c8b82a60d |
||
|
|
9a525f86cc |
||
|
|
791ad0e294 |
||
|
|
bc1ecd1f32 |
||
|
|
39b0cca4b7 |
||
|
|
88be60c0a6 |
||
|
|
d6264dfe1d |
||
|
|
4eb2576e42 |
||
|
|
8293de75b1 |
||
|
|
3083c39215 |
||
|
|
69d63ffd11 |
||
|
|
a4631a19cf |
||
|
|
ee6f026bbb |
||
|
|
3c6d3c0b7a |
||
|
|
c162b8bbeb |
||
|
|
be8353a54b |
||
|
|
9d321bb3ab |
||
|
|
5eb0571d31 |
||
|
|
daaf16bbf8 |
||
|
|
7dfd82c8df |
||
|
|
3c41dcdbce |
||
|
|
98a20e3879 |
||
|
|
d40e67fb2f |
||
|
|
ebd7b6d054 |
||
|
|
5284aa6915 |
||
|
|
124c37108b |
||
|
|
9bbc0588e3 |
||
|
|
315b2583e1 |
||
|
|
8c12403c4f |
||
|
|
13313c5d96 |
||
|
|
b8b94dc802 |
||
|
|
348e7b3360 |
||
|
|
b2934836a9 |
||
|
|
88bb94bd2a |
||
|
|
85dc414627 |
||
|
|
b82a26dadb |
||
|
|
df678831d8 |
||
|
|
e3e0963327 |
||
|
|
f517e554fe |
||
|
|
80bdeb79fc |
||
|
|
64152f804b |
||
|
|
f8da2cb0e5 |
||
|
|
221b6adb8e |
||
|
|
827de250ea |
||
|
|
75f6579efa |
||
|
|
ecc25c402f |
||
|
|
27b46f7f39 |
||
|
|
fbf93dca42 |
||
|
|
231f7c8af4 |
||
|
|
2275a5542e |
||
|
|
80040aaa6d |
||
|
|
f407b98443 |
||
|
|
6756520447 |
||
|
|
3e18e6a4fa |
||
|
|
407577ee8d |
||
|
|
ff69ee5c91 |
||
|
|
98764f2782 |
||
|
|
61491a786c |
||
|
|
179498f28b |
||
|
|
00796dd8ad |
||
|
|
cfd508005d |
||
|
|
b7f8f83944 |
||
|
|
25e040ffb9 |
||
|
|
5543046305 |
||
|
|
9ba9256edb |
||
|
|
04decde5aa |
||
|
|
f1a719cb23 |
||
|
|
e478b93ba0 |
||
|
|
bc9b353975 |
||
|
|
a1ec06d802 |
||
|
|
1c3a03e972 |
||
|
|
446936f7ff |
||
|
|
22f45ac482 |
||
|
|
af4a7e7ab9 |
||
|
|
674b09d38d |
||
|
|
ff4e736cf7 |
||
|
|
0006f74c2d |
||
|
|
98a8465e12 |
||
|
|
8b2cb0fcff |
||
|
|
de9ea429e7 |
||
|
|
0b89140ef1 |
||
|
|
894815f6d7 |
||
|
|
18174c6470 |
||
|
|
8e62c68acb |
||
|
|
65f7cc3a1a |
||
|
|
ee908528d4 |
||
|
|
eab1d4392a |
||
|
|
9461113cfa |
||
|
|
f2eac4c3b3 |
||
|
|
01c4737ec0 |
||
|
|
793a80d6dc |
||
|
|
3f278d2005 |
||
|
|
11866dc217 |
||
|
|
2b87c5f5c2 |
||
|
|
8ae1e4eafd |
||
|
|
9f5ce75454 |
||
|
|
ff753051f7 |
||
|
|
0f23c2c4b1 |
||
|
|
b22332e1d7 |
||
|
|
a620ecca8b |
||
|
|
61d84d0107 |
||
|
|
f975ce1494 |
||
|
|
b68a109591 |
||
|
|
b74f8fc349 |
||
|
|
cb21ff6b6a |
||
|
|
c917bae050 |
||
|
|
5dd4c91fff |
||
|
|
f0b2fc6c4f |
||
|
|
2f5af4d4a1 |
||
|
|
47b51e804f |
||
|
|
20d09941b9 |
||
|
|
550e40f322 |
||
|
|
18e41f243a |
||
|
|
78d74ea3ee |
||
|
|
8ed1f15b95 |
||
|
|
8371fbe9bf |
||
|
|
d5edd53536 |
||
|
|
62587796ce |
||
|
|
67b557349d |
||
|
|
f52da04859 |
||
|
|
16c37cb0ed |
||
|
|
adb391ab5a |
||
|
|
e13bca696b |
||
|
|
65edf5c03a |
||
|
|
c533b039b2 |
||
|
|
6f35e021f9 |
||
|
|
f7b9224bda |
||
|
|
892dae6842 |
||
|
|
0960a714aa |
||
|
|
415a49e04b |
||
|
|
964e9d2343 |
||
|
|
958f50cec3 |
||
|
|
b86d05de08 |
||
|
|
9363049e0f |
||
|
|
4243276698 |
||
|
|
efb90b4216 |
||
|
|
3178e0ee08 |
||
|
|
8510ce45f3 |
||
|
|
d8d17734cb |
||
|
|
b8e0e5c2c1 |
||
|
|
f338a7b91e |
||
|
|
643c430e36 |
||
|
|
e0da0529ec |
||
|
|
f1781719e1 |
||
|
|
4986122493 |
||
|
|
c3c3f0bf75 |
||
|
|
e761ea071b |
||
|
|
697f659083 |
||
|
|
6d3ff0c5ba |
||
|
|
bcdf9af5eb |
||
|
|
5a6dfa0516 |
||
|
|
f237458b35 |
||
|
|
dd01d98553 |
||
|
|
57b6b326c0 |
||
|
|
9d07a852a9 |
||
|
|
694297a6f4 |
||
|
|
f795bf303d |
||
|
|
c581bc14d5 |
||
|
|
98060d48cb |
||
|
|
55d776d110 |
||
|
|
b1c18c7207 |
||
|
|
55bafac6b7 |
||
|
|
66b3049cfc |
||
|
|
5afa838ee8 |
||
|
|
cc934c8b85 |
||
|
|
5893fedef4 |
||
|
|
628d34d7ce |
||
|
|
4586775afc |
||
|
|
5921830423 |
||
|
|
093555dc8d |
||
|
|
5f58e335c8 |
||
|
|
ad15055515 |
||
|
|
a0b64b30e0 |
||
|
|
f3a81c23c3 |
||
|
|
f4fca4f52b |
||
|
|
2d729c436d |
||
|
|
3fe8de1167 |
||
|
|
805c4cdc81 |
||
|
|
938b21c359 |
||
|
|
4f4d673e5a |
||
|
|
7a37c0ccbd |
||
|
|
9ee64b8c66 |
||
|
|
87345118d8 |
||
|
|
aee0e6d866 |
||
|
|
5238966385 |
||
|
|
595a23ca0a | ||
|
|
49d9f873dd |
||
|
|
139b24d11b |
||
|
|
a042d00d25 |
||
|
|
a13ca9c9dd |
||
|
|
aefa5c9e36 |
||
|
|
535a36042c |
||
|
|
3d4a16bdad |
||
|
|
263aa418e2 |
||
|
|
0e835c8130 |
||
|
|
e90760a2f1 |
||
|
|
855d2c8231 |
||
|
|
4250d912ae |
||
|
|
f412ef533a |
||
|
|
0a669b067e |
||
|
|
96423be5b3 |
||
|
|
a31e3c4c53 |
||
|
|
3ff47ea71a |
||
|
|
51a6162514 |
||
|
|
f8e9fcabd0 |
||
|
|
9e98b7bf13 |
||
|
|
c587489765 |
||
|
|
ba1af553e0 |
||
|
|
8532428ec1 |
||
|
|
2d8fbda85d |
||
|
|
c1618155b7 |
||
|
|
bae71b08f1 |
||
|
|
8935965df6 | ||
|
|
a8b46d59a6 |
||
|
|
9a213a7caf |
||
|
|
89b1cb79d0 |
||
|
|
48c6a783ce |
||
|
|
5e291e4cce |
||
|
|
672120990e |
||
|
|
73fc0d747e |
||
|
|
2881ca98dc |
||
|
|
90723676a0 |
||
|
|
bfcabaa2fc |
||
|
|
5863cdd795 |
||
|
|
6ca41dee61 |
||
|
|
0e4c4c775b |
||
|
|
ec5a34f926 |
||
|
|
ed94aeacec |
||
|
|
e80dd3740d |
||
|
|
f7e545f0b5 |
||
|
|
c021de4957 |
||
|
|
cca5da3cc2 |
||
|
|
b14ec35679 |
||
|
|
bd34a617df |
||
|
|
dca35b46d4 |
||
|
|
ea55d5508b |
||
|
|
14d1c9bf59 |
||
|
|
4e87170f0e |
||
|
|
75ea5e710c |
||
|
|
10e55ff241 |
||
|
|
2d5379b61d |
||
|
|
14db1a60e4 |
||
|
|
4f7ec2fc11 |
||
|
|
09850d2ce7 |
||
|
|
826d6244f3 |
||
|
|
1ad460cee6 |
||
|
|
1f0ed8005a |
||
|
|
694af62b3d |
||
|
|
1785e7b42e |
||
|
|
2a17128743 |
||
|
|
13db400356 |
||
|
|
42d691ce4a |
||
|
|
a5c7ad272f |
||
|
|
fa900132b4 |
||
|
|
71fda2f258 |
||
|
|
40ff197bd0 |
||
|
|
315fba975b |
||
|
|
3aacca1ff7 |
||
|
|
dc1c166fd1 |
||
|
|
9e2633e99c |
||
|
|
f11fc4008a |
||
|
|
889ec056f2 |
||
|
|
6d0d317252 |
||
|
|
9b79aac1cf |
||
|
|
f2ef566acc |
||
|
|
184a41ae4f |
||
|
|
8e784b9616 |
||
|
|
395f2a951b |
||
|
|
d778e54f3a |
||
|
|
7494bb6318 |
||
|
|
d7c79c2141 |
||
|
|
3fd89f7113 |
||
|
|
91b29d4103 |
||
|
|
474686a1b2 |
||
|
|
c54133ffb7 |
||
|
|
999cc4d095 |
||
|
|
b9ccb419e6 |
||
|
|
c7dcfa765c |
||
|
|
05589f5b04 |
||
|
|
4b15117894 |
||
|
|
3b2c6606de |
||
|
|
4367572005 |
||
|
|
01f9fb934f |
||
|
|
b6ebb96378 |
||
|
|
6956df67c8 |
||
|
|
e027c484bd |
||
|
|
00980208d2 |
||
|
|
2d3cb91833 |
||
|
|
88e42a05fe |
||
|
|
9429119198 |
||
|
|
0980939f1a |
||
|
|
41c18d2531 |
||
|
|
ac2063d484 |
||
|
|
e5f23c4081 |
||
|
|
28dfc3f532 |
||
|
|
bc281128fd |
||
|
|
a418753dae |
||
|
|
5c32a14aaf |
||
|
|
4f5b4e807f |
||
|
|
c2d5facce2 |
||
|
|
d83eece45c |
||
|
|
a697c40686 |
||
|
|
d80b9072a7 |
||
|
|
d295ebdf81 |
||
|
|
9dfcf3c75a |
||
|
|
b34b8acb6d |
||
|
|
cfd8cf6718 |
||
|
|
56f67097a9 |
||
|
|
c5055a8916 |
||
|
|
f3e7599d3c |
||
|
|
e749c7038a |
||
|
|
66183084d5 |
||
|
|
1c1805e342 |
||
|
|
178503fef7 |
||
|
|
030e20f5e4 |
||
|
|
1b0ed17460 |
||
|
|
78347e8491 |
||
|
|
a2a7cf0476 |
||
|
|
9f9c1ae131 |
||
|
|
8613bc85cb |
||
|
|
7e3be8f8a4 |
||
|
|
962f0254b1 |
||
|
|
63b658574a |
||
|
|
89c71e4a3c |
||
|
|
628aedef9e |
||
|
|
426df7e083 |
||
|
|
e6dd45b2cc |
||
|
|
b7989d6aa8 |
||
|
|
9d47022075 |
||
|
|
b6e25424a3 |
||
|
|
9a992f365b |
||
|
|
c8968e5171 |
||
|
|
105518b1ee |
||
|
|
70e01b4816 |
||
|
|
5b041b795c |
||
|
|
89522b05cf |
||
|
|
88179b23a5 |
||
|
|
a068a49527 |
||
|
|
72d34230d8 |
||
|
|
40bd1d71ce |
||
|
|
6a145bd893 |
||
|
|
c06fe02c61 |
||
|
|
dd34b817f2 |
||
|
|
12c16bce61 |
||
|
|
41728572fe |
||
|
|
91220431be |
||
|
|
32ea6c9942 |
||
|
|
374476a419 |
||
|
|
d745f14f5c | ||
|
|
bee1175198 |
||
|
|
ad83000fe7 |
||
|
|
697e6242d6 |
||
|
|
b3ab49f22f |
||
|
|
0c9d4970c4 |
||
|
|
f6cc1b867a |
||
|
|
e78a730a13 |
||
|
|
c132ea25fa |
||
|
|
32b99b6a52 |
||
|
|
6d2ee56ada |
||
|
|
9c284a4426 |
||
|
|
4409f91e28 |
||
|
|
c00fc82777 |
||
|
|
e39886369f |
||
|
|
5892baa3bb |
||
|
|
1a74ea87bd |
||
|
|
51326069e2 |
||
|
|
2c5a83b175 |
||
|
|
8ee19e3bcf |
||
|
|
be868fd4af |
||
|
|
8c6c725dd9 |
||
|
|
add8341680 |
||
|
|
ba4bb26067 |
||
|
|
521e5c0ff8 |
||
|
|
11340aa436 |
||
|
|
2c21aeed3a |
||
|
|
23513e9159 |
||
|
|
b1451dd447 |
||
|
|
560b795d58 |
||
|
|
62e7c48b2c |
||
|
|
259ebd877a |
||
|
|
fe34c5b469 |
||
|
|
bb996a7571 |
||
|
|
9e18a6bdd3 |
||
|
|
f20f0a3ccb |
||
|
|
b46c76c6d0 |
||
|
|
bb8099228c |
||
|
|
d084ea0e56 |
||
|
|
d97832a5f8 |
||
|
|
46ae7de9a6 |
||
|
|
a6376a706c |
||
|
|
764101c192 |
||
|
|
b7fdde761c |
||
|
|
11dd532502 |
||
|
|
dd45d0e0d8 |
||
|
|
efb9771f47 |
||
|
|
73434ac0cf |
||
|
|
210aec62db |
||
|
|
3d0db426fa |
||
|
|
d1a57b34af |
||
|
|
78c957a607 |
||
|
|
c8444b4ecd |
||
|
|
0b4fc5f7c7 |
||
|
|
87e42caeee |
||
|
|
639588f550 |
||
|
|
7e73f0e290 |
||
|
|
9769d4fdda |
||
|
|
f329a58334 |
||
|
|
4aabe7febf |
||
|
|
bc1b81e23a |
||
|
|
67b52f4f18 |
||
|
|
1a12fca8dd |
||
|
|
90d69a727a |
||
|
|
8c475737e1 |
||
|
|
cc8064a08e |
||
|
|
1300d8d970 |
||
|
|
42408e5b34 |
||
|
|
fb948069d2 |
||
|
|
8d4fac7181 |
||
|
|
8bd940e75c |
||
|
|
c444a740ba |
||
|
|
0ca882562a |
||
|
|
81b0e1a8dd |
||
|
|
cae964e601 |
||
|
|
d3798a3174 |
||
|
|
1e02834c48 |
||
|
|
7d571ed73a |
||
|
|
595628dcf4 |
||
|
|
542c764d25 |
||
|
|
ac69fa3a0c |
||
|
|
d55d9097dc |
||
|
|
4b2bc57e25 |
||
|
|
536b47e4a3 |
||
|
|
fcf912bd15 |
||
|
|
1c1ede2d71 |
||
|
|
e36c21d046 |
||
|
|
3e6428d72f |
||
|
|
0942c05dbd |
||
|
|
613806be14 |
||
|
|
3b66fcea80 |
||
|
|
0d1cee1b6b |
||
|
|
f07a42529b |
||
|
|
ad78c817b9 |
||
|
|
6a512839b7 |
||
|
|
5bc0c30c1f |
||
|
|
9f07a09877 |
||
|
|
bcc5bccbf0 |
||
|
|
d363620120 |
||
|
|
fd1580998e |
||
|
|
85b85feeb8 |
||
|
|
3af360944a |
||
|
|
0c134a0a6f |
||
|
|
9c7b3b9547 |
||
|
|
9235b80125 |
||
|
|
cdd751b0e0 |
||
|
|
a7c4d55912 |
||
|
|
238e56e39c |
||
|
|
46a20e5041 |
||
|
|
f72e606628 |
||
|
|
80e3463ab3 |
||
|
|
6b92af898d |
||
|
|
5c0ebc4435 |
||
|
|
0a14067460 |
||
|
|
ac900616a5 |
||
|
|
ddad70b4c5 |
||
|
|
8375b985e8 |
||
|
|
9fea05769f |
||
|
|
762ed8fe59 |
||
|
|
f537711e14 |
||
|
|
5da0a5d4b0 |
||
|
|
6f30f0268e |
||
|
|
e1bd64c156 |
||
|
|
4a28c99635 |
||
|
|
6133563fe2 |
||
|
|
899a9b6a61 |
||
|
|
f1d5b3b5bd |
||
|
|
8c2e314b31 |
||
|
|
667f29507a |
||
|
|
bdb2a816f8 |
||
|
|
7b909f4ec6 |
||
|
|
e3045caeb0 |
||
|
|
d2d411a7ca |
||
|
|
58cd75be3c |
||
|
|
29a40bf4b9 |
||
|
|
bb5a2a5cf7 |
||
|
|
ec9755d10f |
||
|
|
29fa2b5705 |
||
|
|
6dfd31a030 |
||
|
|
0c77c67353 |
||
|
|
283919fef6 |
||
|
|
4ab2fc0227 |
||
|
|
10ca421026 |
||
|
|
f161048fe7 |
||
|
|
af87f11f3c |
||
|
|
1ec2d9d83b |
||
|
|
91b03d3d25 |
||
|
|
dbd13da119 |
||
|
|
47a7a9591a |
||
|
|
c7cbfcdce2 |
||
|
|
b35d7e9c57 |
||
|
|
1bfec37047 |
||
|
|
ac35a0449d |
||
|
|
df757be9cf |
||
|
|
ad63fb1c54 |
||
|
|
6b04363faf |
||
|
|
a03be515d2 |
||
|
|
1a1d2ad708 |
||
|
|
4c33de41a3 |
||
|
|
cdb3472c13 |
||
|
|
36af1e86f7 |
||
|
|
865ddc308a |
||
|
|
b74690defb |
||
|
|
ad11b53932 |
||
|
|
257c8736c6 |
||
|
|
eced2ffdd7 |
||
|
|
6be0fb6828 |
||
|
|
b1162cc2da |
||
|
|
0fb022e751 |
||
|
|
b952cb58de |
||
|
|
73b225c5a7 |
||
|
|
dfdfe1b62a |
||
|
|
2e2af9b9ff |
||
|
|
8814fc811b |
||
|
|
ece824d67b |
||
|
|
38f403f88c |
||
|
|
af69bbf1e8 |
||
|
|
ffa316b5e2 |
||
|
|
e0e6cb6e9d |
||
|
|
9ef23270b4 |
||
|
|
420f7eb0ba |
||
|
|
6fa6dcc533 |
||
|
|
5c4ffdc7a2 |
||
|
|
e8c4a02945 |
||
|
|
8ceab4f8ce |
||
|
|
47f05c9163 |
||
|
|
93fdf689c2 |
||
|
|
5e7a1df2b6 |
||
|
|
702de28ceb |
||
|
|
722c7e077c |
||
|
|
6bb8b3fb5f |
||
|
|
01c5ec131c |
||
|
|
5a975a1177 |
||
|
|
d6e5975f94 |
||
|
|
d6ce4adaa6 |
||
|
|
1ce202f987 |
||
|
|
0daf48ae2b |
||
|
|
7aad88e192 |
||
|
|
403a4417cb |
||
|
|
ea5eb424d6 |
||
|
|
fa4b532a98 |
||
|
|
151ebeb45f |
||
|
|
40be134746 |
||
|
|
f2ce33478b |
||
|
|
902911b850 |
||
|
|
ed11ef775c |
||
|
|
53f736cb2e |
||
|
|
3435287cac |
||
|
|
0fedc6828f |
||
|
|
ca2d6eaf3d |
||
|
|
0a351256f0 |
||
|
|
63ab3ccffc |
||
|
|
1212c6021f |
||
|
|
498ebb333e |
||
|
|
7bd4bf96f5 |
||
|
|
271b45c963 |
||
|
|
6c9bfd7b84 |
||
|
|
6206678e74 |
||
|
|
83e50d030e |
||
|
|
953dc28e94 |
||
|
|
a5ce4aa8f4 |
||
|
|
c830403cd7 |
||
|
|
3e9bace0ad |
||
|
|
957d930613 |
||
|
|
6bb2582b61 |
||
|
|
a5e41e4822 |
||
|
|
7cd55c741d |
||
|
|
864fc6b46b |
||
|
|
d10469e1a6 |
||
|
|
52ed2f0243 |
||
|
|
00a9a6ee44 |
||
|
|
3e9d02be00 |
||
|
|
3cbfe2f77c |
||
|
|
951532d3b3 |
||
|
|
bfc153c2e6 |
||
|
|
36f2f5026f |
||
|
|
411cf34437 |
||
|
|
bfc4d7facf |
||
|
|
53ff3d39e7 |
||
|
|
6c5eb78cc2 |
||
|
|
1736199650 |
||
|
|
75d311c67c |
||
|
|
447bdb827c |
||
|
|
a7a5e889c9 |
||
|
|
b49e3d01fa |
||
|
|
594e764560 |
||
|
|
2a5c7ff90b |
||
|
|
a15cfde3d7 |
||
|
|
08e0e1a1bf |
||
|
|
6839f624fb |
||
|
|
9fe4fd7911 |
||
|
|
ea3f2af2fb |
||
|
|
00d26fdee1 |
||
|
|
730a0c8709 |
||
|
|
ec9cd3f209 |
||
|
|
f99977718b |
||
|
|
4bcd4f9d3a |
||
|
|
6369cc8f3e |
||
|
|
2827506ac5 |
||
|
|
99fe6686e2 |
||
|
|
01af3085bb |
||
|
|
c4d4aacc4e |
||
|
|
088c809c6f |
||
|
|
1606a80ee1 |
||
|
|
934422d05e | ||
|
|
46ca53dad7 |
||
|
|
0900b4c85c |
||
|
|
1cee8ebbfd |
||
|
|
0f9cee7773 |
||
|
|
24dd3f08ef |
||
|
|
32a86fdd76 |
||
|
|
3ea226c890 |
||
|
|
62141f97f6 |
||
|
|
1f0a61a032 | ||
|
|
d4cdc059bf |
||
|
|
077a11d86b |
||
|
|
e1470068ff |
||
|
|
b4ace199f2 |
||
|
|
c24d5fd055 |
||
|
|
d3fcabaf43 |
||
|
|
d25c8dd8ab |
||
|
|
08669dc6ea |
||
|
|
39ea15c9b3 |
||
|
|
d577358df3 |
||
|
|
b68c6842f4 |
||
|
|
85bf1fa398 |
||
|
|
0fbbe0cbd9 |
||
|
|
396da6ea28 |
||
|
|
4690be3e3c |
||
|
|
1232bfb3b3 |
||
|
|
287491fc1c |
||
|
|
864ac6d38a |
||
|
|
064e489424 |
||
|
|
93df9293c6 |
||
|
|
5328036969 |
||
|
|
cffcf084f8 |
||
|
|
0ee43b5a8a |
||
|
|
0ff6455601 |
||
|
|
afacaa72c6 |
||
|
|
f3f7346f62 |
||
|
|
03a4d27d82 |
||
|
|
f7bdb431c5 |
||
|
|
896b77438c |
||
|
|
397f645b08 |
||
|
|
c0d640dbd9 |
||
|
|
752c3cffd5 |
||
|
|
29cb0d7a4b |
||
|
|
5c459239bf |
||
|
|
34fc59cbe5 |
||
|
|
c210156054 |
||
|
|
a2baa3e4d8 |
||
|
|
ca07f41e78 |
||
|
|
972f0db360 |
||
|
|
06119ce07b |
||
|
|
a747190d55 |
||
|
|
fa55bc77b0 |
||
|
|
0fba4e5920 |
||
|
|
8ddeedce8b |
||
|
|
f7efc3f155 |
||
|
|
97452ae8d5 |
||
|
|
de315082b8 |
||
|
|
dd461d5f48 |
||
|
|
eadded9aa2 |
||
|
|
45ef6fd143 |
||
|
|
70eccc6f6e |
||
|
|
5d44577394 |
||
|
|
dcda0984fa |
||
|
|
b719a5602a |
||
|
|
ca62fb55f6 |
||
|
|
471469f1c8 |
||
|
|
9747e4d8e0 |
||
|
|
ae796fc1bc |
||
|
|
d6b52ab86e |
||
|
|
cbf3a3d86f |
||
|
|
8b61ebe132 |
||
|
|
805b6da760 |
||
|
|
bcac66cd7c |
||
|
|
a730417b3b |
||
|
|
9606c212b0 | ||
|
|
82a73659b7 |
||
|
|
d4b2e76575 |
||
|
|
b8afbd0366 |
328 changed files with 56623 additions and 30006 deletions
122
.codecov.yml
Normal file
122
.codecov.yml
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 2%
|
||||
|
||||
comment:
|
||||
layout: "header, diff, flags, components, files"
|
||||
after_n_builds: 2
|
||||
|
||||
ignore:
|
||||
- "*_easyjson.go"
|
||||
- "**/*_easyjson.go"
|
||||
- "*.pb.go"
|
||||
- "**/*.pb.go"
|
||||
|
||||
component_management:
|
||||
individual_components:
|
||||
- component_id: module_root
|
||||
name: root
|
||||
paths:
|
||||
- "*.go"
|
||||
- component_id: module_api
|
||||
name: api
|
||||
paths:
|
||||
- api/**
|
||||
- component_id: module_async
|
||||
name: async
|
||||
paths:
|
||||
- async/**
|
||||
- component_id: module_client
|
||||
name: client
|
||||
paths:
|
||||
- client/**
|
||||
- component_id: module_cmd_client
|
||||
name: cmd/client
|
||||
paths:
|
||||
- cmd/client/**
|
||||
- component_id: module_cmd_proxy
|
||||
name: cmd/proxy
|
||||
paths:
|
||||
- cmd/proxy/**
|
||||
- component_id: module_cmd_server
|
||||
name: cmd/server
|
||||
paths:
|
||||
- cmd/server/**
|
||||
- component_id: module_config
|
||||
name: config
|
||||
paths:
|
||||
- config/**
|
||||
- component_id: module_container
|
||||
name: container
|
||||
paths:
|
||||
- container/**
|
||||
- component_id: module_dns
|
||||
name: dns
|
||||
paths:
|
||||
- dns/**
|
||||
- component_id: module_etcd
|
||||
name: etcd
|
||||
paths:
|
||||
- etcd/**
|
||||
- component_id: module_geoip
|
||||
name: geoip
|
||||
paths:
|
||||
- geoip/**
|
||||
- component_id: module_grpc
|
||||
name: grpc
|
||||
paths:
|
||||
- grpc/**
|
||||
- component_id: module_internal
|
||||
name: internal
|
||||
paths:
|
||||
- internal/**
|
||||
- component_id: module_log
|
||||
name: log
|
||||
paths:
|
||||
- log/**
|
||||
- component_id: module_metrics
|
||||
name: metrics
|
||||
paths:
|
||||
- metrics/**
|
||||
- component_id: module_mock
|
||||
name: mock
|
||||
paths:
|
||||
- mock/**
|
||||
- component_id: module_nats
|
||||
name: nats
|
||||
paths:
|
||||
- nats/**
|
||||
- component_id: module_pool
|
||||
name: pool
|
||||
paths:
|
||||
- pool/**
|
||||
- component_id: module_proxy
|
||||
name: proxy
|
||||
paths:
|
||||
- proxy/**
|
||||
- component_id: module_security
|
||||
name: security
|
||||
paths:
|
||||
- security/**
|
||||
- component_id: module_server
|
||||
name: server
|
||||
paths:
|
||||
- server/**
|
||||
- component_id: module_session
|
||||
name: session
|
||||
paths:
|
||||
- session/**
|
||||
- component_id: module_sfu
|
||||
name: sfu
|
||||
paths:
|
||||
- sfu/**
|
||||
- component_id: module_talk
|
||||
name: talk
|
||||
paths:
|
||||
- talk/**
|
||||
- component_id: module_test
|
||||
name: test
|
||||
paths:
|
||||
- test/**
|
||||
2
.github/workflows/check-continentmap.yml
vendored
2
.github/workflows/check-continentmap.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check continentmap
|
||||
run: make check-continentmap
|
||||
|
|
|
|||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -36,15 +36,15 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
|
|
|||
6
.github/workflows/command-rebase.yml
vendored
6
.github/workflows/command-rebase.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Add reaction on start
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
repository: ${{ github.event.repository.full_name }}
|
||||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
reaction-type: "+1"
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Add reaction on failure
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
if: failure()
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
|
|
|||
52
.github/workflows/deploydocker.yml
vendored
52
.github/workflows/deploydocker.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Deploy to Docker Hub / GHCR
|
||||
name: Deploy to Docker Hub / GHCR / quay.io
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
@ -27,18 +27,19 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
strukturag/nextcloud-spreed-signaling
|
||||
ghcr.io/strukturag/nextcloud-spreed-signaling
|
||||
quay.io/strukturag/nextcloud-spreed-signaling
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
|
|
@ -46,7 +47,7 @@ jobs:
|
|||
type=semver,pattern={{major}}
|
||||
type=sha,prefix=
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
|
@ -54,26 +55,34 @@ jobs:
|
|||
${{ runner.os }}-buildx-
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to quay.io
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_IO_USERNAME }}
|
||||
password: ${{ secrets.QUAY_IO_ACCESS_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/server/Dockerfile
|
||||
|
|
@ -92,18 +101,19 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
strukturag/nextcloud-spreed-signaling
|
||||
ghcr.io/strukturag/nextcloud-spreed-signaling
|
||||
quay.io/strukturag/nextcloud-spreed-signaling
|
||||
labels: |
|
||||
org.opencontainers.image.title=nextcloud-spreed-signaling-proxy
|
||||
org.opencontainers.image.description=Signaling proxy for the standalone signaling server for Nextcloud Talk.
|
||||
|
|
@ -116,7 +126,7 @@ jobs:
|
|||
type=semver,pattern={{major}}
|
||||
type=sha,prefix=
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
|
@ -124,26 +134,34 @@ jobs:
|
|||
${{ runner.os }}-buildx-
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to quay.io
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_IO_USERNAME }}
|
||||
password: ${{ secrets.QUAY_IO_ACCESS_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/proxy/Dockerfile
|
||||
|
|
|
|||
18
.github/workflows/docker-compose.yml
vendored
18
.github/workflows/docker-compose.yml
vendored
|
|
@ -22,26 +22,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update docker-compose
|
||||
run: |
|
||||
curl -SL https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-linux-x86_64 -o docker-compose
|
||||
chmod a+x docker-compose
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Pull Docker images
|
||||
run: ./docker-compose -f docker/docker-compose.yml pull
|
||||
run: docker compose -f docker/docker-compose.yml pull
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update docker-compose
|
||||
run: |
|
||||
curl -SL https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-linux-x86_64 -o docker-compose
|
||||
chmod a+x docker-compose
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build Docker images
|
||||
run: ./docker-compose -f docker/docker-compose.yml build
|
||||
run: docker compose -f docker/docker-compose.yml build
|
||||
|
|
|
|||
6
.github/workflows/docker-janus.yml
vendored
6
.github/workflows/docker-janus.yml
vendored
|
|
@ -23,13 +23,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker/janus
|
||||
load: true
|
||||
|
|
|
|||
16
.github/workflows/docker.yml
vendored
16
.github/workflows/docker.yml
vendored
|
|
@ -33,16 +33,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/server/Dockerfile
|
||||
|
|
@ -52,16 +52,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/proxy/Dockerfile
|
||||
|
|
|
|||
37
.github/workflows/generated.yml
vendored
37
.github/workflows/generated.yml
vendored
|
|
@ -5,20 +5,24 @@ on:
|
|||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/generated.yml'
|
||||
- 'api*.go'
|
||||
- '*_easyjson.go'
|
||||
- '*.pb.go'
|
||||
- '*.proto'
|
||||
- '**/api*.go'
|
||||
- '**/*_easyjson.go'
|
||||
- '**/*.pb.go'
|
||||
- '**/*.proto'
|
||||
- 'go.*'
|
||||
- 'api/signaling.go'
|
||||
- 'talk/ocs.go'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/generated.yml'
|
||||
- 'api*.go'
|
||||
- '*_easyjson.go'
|
||||
- '*.pb.go'
|
||||
- '*.proto'
|
||||
- '**/api*.go'
|
||||
- '**/*_easyjson.go'
|
||||
- '**/*.pb.go'
|
||||
- '**/*.proto'
|
||||
- 'go.*'
|
||||
- 'api/signaling.go'
|
||||
- 'talk/ocs.go'
|
||||
|
||||
env:
|
||||
CODE_GENERATOR_NAME: struktur AG service user
|
||||
|
|
@ -53,12 +57,12 @@ jobs:
|
|||
contents: write
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.CODE_GENERATOR_PAT }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "stable"
|
||||
|
||||
|
|
@ -78,16 +82,15 @@ jobs:
|
|||
if [ "$CHECKOUT_SHA" != "${{github.event.pull_request.head.sha}}" ]; then
|
||||
echo "More changes since this commit ${{github.event.pull_request.head.sha}}, skipping"
|
||||
else
|
||||
git add *_easyjson.go *.pb.go
|
||||
git add --all
|
||||
CHANGES=$(git status --porcelain)
|
||||
if [ -z "$CHANGES" ]; then
|
||||
echo "No files have changed, no need to commit / push."
|
||||
else
|
||||
go mod tidy
|
||||
git add go.*
|
||||
git config user.name "$CODE_GENERATOR_NAME"
|
||||
git config user.email "$CODE_GENERATOR_EMAIL"
|
||||
git commit --author="$(git log -n 1 --pretty=format:%an) <$(git log -n 1 --pretty=format:%ae)>" -m "Update generated files from ${{github.event.pull_request.head.sha}}"
|
||||
git commit --all --author="$(git log -n 1 --pretty=format:%an) <$(git log -n 1 --pretty=format:%ae)>" -m "Update generated files from ${{github.event.pull_request.head.sha}}"
|
||||
git push
|
||||
fi
|
||||
fi
|
||||
|
|
@ -97,8 +100,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "stable"
|
||||
|
||||
|
|
@ -113,5 +116,5 @@ jobs:
|
|||
|
||||
- name: Check generated files
|
||||
run: |
|
||||
git add *.go
|
||||
git diff --cached --exit-code *.go
|
||||
git add --all
|
||||
git diff --cached --exit-code
|
||||
|
|
|
|||
9
.github/workflows/govuln.yml
vendored
9
.github/workflows/govuln.yml
vendored
|
|
@ -24,13 +24,14 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.22"
|
||||
- "1.23"
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
|
||||
- run: date
|
||||
|
||||
|
|
|
|||
4
.github/workflows/licensecheck.yml
vendored
4
.github/workflows/licensecheck.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install licensecheck
|
||||
run: |
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
run: |
|
||||
{
|
||||
echo 'CHECK_RESULT<<EOF'
|
||||
licensecheck *.go */*.go
|
||||
find -name "*.go" | sort | xargs licensecheck
|
||||
echo EOF
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
|
|
|
|||
45
.github/workflows/lint.yml
vendored
45
.github/workflows/lint.yml
vendored
|
|
@ -8,6 +8,7 @@ on:
|
|||
- '.golangci.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
|
|
@ -15,6 +16,7 @@ on:
|
|||
- '.golangci.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -25,31 +27,60 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: "1.25"
|
||||
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v6.2.0
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=2m0s
|
||||
skip-cache: true
|
||||
|
||||
modernize:
|
||||
name: modernize
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- name: moderize
|
||||
run: |
|
||||
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -any=false -reflecttypefor=false -test ./...
|
||||
|
||||
checklocks:
|
||||
name: checklocks
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
check-latest: true
|
||||
|
||||
- name: checklocks
|
||||
run: |
|
||||
make checklocks
|
||||
|
||||
dependencies:
|
||||
name: dependencies
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "stable"
|
||||
|
||||
- name: Check minimum supported version of Go
|
||||
run: |
|
||||
go mod tidy -go=1.22.0 -compat=1.22.0
|
||||
go mod tidy -go=1.25.0 -compat=1.25.0
|
||||
|
||||
- name: Check go.mod / go.sum
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/shellcheck.yml
vendored
2
.github/workflows/shellcheck.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
name: shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: shellcheck
|
||||
run: |
|
||||
|
|
|
|||
68
.github/workflows/tarball.yml
vendored
68
.github/workflows/tarball.yml
vendored
|
|
@ -24,12 +24,12 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.22"
|
||||
- "1.23"
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
|
|
@ -39,26 +39,71 @@ jobs:
|
|||
make tarball
|
||||
|
||||
- name: Upload tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tarball-${{ matrix.go-version }}
|
||||
path: nextcloud-spreed-signaling*.tar.gz
|
||||
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create]
|
||||
steps:
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Download tarball
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: tarball-${{ matrix.go-version }}
|
||||
|
||||
- name: Extract tarball
|
||||
run: |
|
||||
mkdir -p tmp
|
||||
tar xvf nextcloud-spreed-signaling*.tar.gz --strip-components=1 -C tmp
|
||||
[ -d "tmp/vendor" ] || exit 1
|
||||
[ -f "tmp/version.txt" ] || exit 1
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
echo "Building with $(nproc) threads"
|
||||
make -C tmp build client -j$(nproc)
|
||||
UNKNOWN=$(./tmp/bin/signaling -version | grep unknown || true)
|
||||
if [ -n "$UNKNOWN" ]; then \
|
||||
echo "Found unknown version: $UNKNOWN"; \
|
||||
exit 1; \
|
||||
fi
|
||||
UNKNOWN=$(./tmp/bin/proxy -version | grep unknown || true)
|
||||
if [ -n "$UNKNOWN" ]; then \
|
||||
echo "Found unknown version: $UNKNOWN"; \
|
||||
exit 1; \
|
||||
fi
|
||||
UNKNOWN=$(./tmp/bin/client -version | grep unknown || true)
|
||||
if [ -n "$UNKNOWN" ]; then \
|
||||
echo "Found unknown version: $UNKNOWN"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.22"
|
||||
- "1.23"
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create]
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Download tarball
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: tarball-${{ matrix.go-version }}
|
||||
|
||||
|
|
@ -68,11 +113,6 @@ jobs:
|
|||
tar xvf nextcloud-spreed-signaling*.tar.gz --strip-components=1 -C tmp
|
||||
[ -d "tmp/vendor" ] || exit 1
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
echo "Building with $(nproc) threads"
|
||||
make -C tmp build -j$(nproc)
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
|
|
|
|||
98
.github/workflows/test.yml
vendored
98
.github/workflows/test.yml
vendored
|
|
@ -5,6 +5,7 @@ on:
|
|||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/test.yml'
|
||||
- '.codecov.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
|
|
@ -12,6 +13,7 @@ on:
|
|||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/test.yml'
|
||||
- '.codecov.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
|
|
@ -20,19 +22,16 @@ permissions:
|
|||
contents: read
|
||||
|
||||
jobs:
|
||||
go:
|
||||
env:
|
||||
MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }}
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.22"
|
||||
- "1.23"
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
|
|
@ -43,38 +42,69 @@ jobs:
|
|||
make proxy -j$(nproc)
|
||||
make server -j$(nproc)
|
||||
|
||||
go:
|
||||
env:
|
||||
MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }}
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test TIMEOUT=120s
|
||||
|
||||
benchmark:
|
||||
env:
|
||||
MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }}
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Run benchmarks
|
||||
run: |
|
||||
make benchmark
|
||||
|
||||
coverage:
|
||||
env:
|
||||
MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }}
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.25"
|
||||
- "1.26"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
make cover TIMEOUT=120s
|
||||
echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV
|
||||
|
||||
- name: Convert coverage to lcov
|
||||
uses: jandelgado/gcov2lcov-action@v1.1.1
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
infile: cover.out
|
||||
outfile: cover.lcov
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@v2.3.4
|
||||
env:
|
||||
COVERALLS_FLAG_NAME: run-${{ matrix.go-version }}
|
||||
with:
|
||||
path-to-lcov: cover.lcov
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel: true
|
||||
|
||||
finish:
|
||||
permissions:
|
||||
contents: none
|
||||
needs: go
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v2.3.4
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel-finished: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./cover.out
|
||||
flags: go-${{ matrix.go-version }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
bin/
|
||||
tmp/
|
||||
vendor/
|
||||
|
||||
*.pem
|
||||
|
|
|
|||
108
.golangci.yml
108
.golangci.yml
|
|
@ -1,34 +1,80 @@
|
|||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- errchkjson
|
||||
- exptostd
|
||||
- gocritic
|
||||
- misspell
|
||||
- modernize
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- revive
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
ignoreGeneratedHeader: true
|
||||
severity: warning
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
#- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
#- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
#- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
#- name: unused-parameter
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
- testifylint
|
||||
settings:
|
||||
errchkjson:
|
||||
check-error-free-encoding: true
|
||||
report-no-exported: true
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- singleCaseSwitch
|
||||
settings:
|
||||
ifElseChain:
|
||||
# Min number of if-else blocks that makes the warning trigger.
|
||||
# Default: 2
|
||||
minThreshold: 3
|
||||
govet:
|
||||
enable:
|
||||
- nilness
|
||||
disable:
|
||||
- stdversion
|
||||
revive:
|
||||
severity: warning
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
#- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
#- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
#- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
#- name: unused-parameter
|
||||
- name: unreachable-code
|
||||
- name: use-any
|
||||
- name: redefines-builtin-id
|
||||
testifylint:
|
||||
disable:
|
||||
- require-error
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
|
|
|||
559
CHANGELOG.md
559
CHANGELOG.md
|
|
@ -2,6 +2,565 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 2.1.1 - 2026-03-12
|
||||
|
||||
### Changed
|
||||
- Drop support for Golang 1.24
|
||||
[#1197](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1197)
|
||||
- Simplify error type checks.
|
||||
[#1190](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1190)
|
||||
- readme: Add example websocket urls for Janus events.
|
||||
[#1191](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1191)
|
||||
- CI: Test with Golang 1.26
|
||||
[#1196](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1196)
|
||||
- CI: Use "docker compose" instead of downloading docker-compose binary.
|
||||
[#1203](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1203)
|
||||
- docker: pin spreedbackend uid and add user group
|
||||
[#1202](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1202)
|
||||
- Bump module version to "v2" so versions 2.x can be imported by others.
|
||||
[#1211](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1211)
|
||||
- Update generated files for v2 module.
|
||||
[#1218](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1218)
|
||||
- Simplify async notifier code
|
||||
[#1220](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1220)
|
||||
|
||||
### Fixed
|
||||
- Update "go.opentelemetry.io/otel/sdk" to fix "GO-2026-4394".
|
||||
[#1207](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1207)
|
||||
- Don't limit size of received Janus events.
|
||||
[#1208](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1208)
|
||||
|
||||
### Dependencies
|
||||
- Bump google.golang.org/grpc/cmd/protoc-gen-go-grpc from 1.6.0 to 1.6.1
|
||||
[#1189](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1189)
|
||||
- Bump markdown from 3.10.1 to 3.10.2 in /docs
|
||||
[#1192](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1192)
|
||||
- Bump github.com/pion/dtls/v3 from 3.0.10 to 3.1.0
|
||||
[#1193](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1193)
|
||||
- Bump golang from 1.25-alpine to 1.26-alpine in /docker/proxy
|
||||
[#1194](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1194)
|
||||
- Bump golang from 1.25-alpine to 1.26-alpine in /docker/server
|
||||
[#1195](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1195)
|
||||
- Bump google.golang.org/grpc from 1.78.0 to 1.79.1
|
||||
[#1201](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1201)
|
||||
- Bump github.com/pion/dtls/v3 from 3.1.0 to 3.1.1
|
||||
[#1199](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1199)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1200](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1200)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.17 to 3.0.18
|
||||
[#1204](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1204)
|
||||
- Bump github.com/pion/ice/v4 from 4.2.0 to 4.2.1
|
||||
[#1205](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1205)
|
||||
- Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0
|
||||
[#1206](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1206)
|
||||
- Bump the artifacts group with 2 updates
|
||||
[#1209](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1209)
|
||||
- Bump module version to "v2" so versions 2.x can be imported by others.
|
||||
[#1211](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1211)
|
||||
- Bump docker/login-action from 3 to 4
|
||||
[#1212](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1212)
|
||||
- Bump docker/setup-qemu-action from 3 to 4
|
||||
[#1213](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1213)
|
||||
- Bump docker/metadata-action from 5 to 6
|
||||
[#1214](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1214)
|
||||
- Bump docker/setup-buildx-action from 3 to 4
|
||||
[#1215](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1215)
|
||||
- Bump docker/build-push-action from 6 to 7
|
||||
[#1217](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1217)
|
||||
- Bump google.golang.org/grpc from 1.79.1 to 1.79.2
|
||||
[#1216](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1216)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.12.4 to 2.12.5
|
||||
[#1219](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1219)
|
||||
|
||||
|
||||
## 2.1.0 - 2026-02-03
|
||||
|
||||
### Added
|
||||
- Introduce "internal" package
|
||||
[#1082](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1082)
|
||||
- Add etcd TLS tests.
|
||||
[#1084](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1084)
|
||||
- Add missing stats registration.
|
||||
[#1086](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1086)
|
||||
- Add commands to the readme on how to build Docker images locally.
|
||||
[#1088](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1088)
|
||||
- Use gvisor checklocks for static lock analysis.
|
||||
[#1078](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1078)
|
||||
- Support relaying of chat messages.
|
||||
[#868](https://github.com/strukturag/nextcloud-spreed-signaling/pull/868)
|
||||
- Return bandwidth information in room responses.
|
||||
[#1099](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1099)
|
||||
- Expose real bandwidth usage through metrics.
|
||||
[#1102](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1102)
|
||||
- Add type to store bandwidths.
|
||||
[#1108](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1108)
|
||||
- Add more WebRTC-related metrics
|
||||
[#1109](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1109)
|
||||
- Add metrics about client bytes/messages sent/received.
|
||||
[#1134](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1134)
|
||||
- Introduce transient session data.
|
||||
[#1120](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1120)
|
||||
- Include "version.txt" in tarball.
|
||||
[#1142](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1142)
|
||||
- CI: Also upload images to quay.io/strukturag/nextcloud-spreed-signaling
|
||||
[#1159](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1159)
|
||||
- Add more metrics about sessions in calls.
|
||||
[#1183](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1183)
|
||||
|
||||
### Changed
|
||||
- dockerfile: create system user instead of normal user, avoid home directory
|
||||
[#1058](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1058)
|
||||
- Use "testing/synctest" to simplify timing-dependent tests.
|
||||
[#1067](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1067)
|
||||
- Add dedicated types for different session ids.
|
||||
[#1066](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1066)
|
||||
- Move "StringMap" class to api module.
|
||||
[#1077](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1077)
|
||||
- CI: Disable "stdversion" check of govet.
|
||||
[#1079](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1079)
|
||||
- CI: Use codecov components.
|
||||
[#1080](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1080)
|
||||
- Add interface for method "GetInCall".
|
||||
[#1083](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1083)
|
||||
- Make LruCache typed through generics.
|
||||
[#1085](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1085)
|
||||
- Protect access to the debug pprof handlers.
|
||||
[#1094](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1094)
|
||||
- Don't use environment to keep per-test properties.
|
||||
[#1106](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1106)
|
||||
- Add formatting to bandwidth values.
|
||||
[#1114](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1114)
|
||||
- Don't format zero bandwidth as "unlimited".
|
||||
[#1115](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1115)
|
||||
- CI: Split test jobs to speed up total actions time.
|
||||
[#1118](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1118)
|
||||
- Stop using global logger
|
||||
[#1117](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1117)
|
||||
- CI: Split tarball jobs to speed up total actions time.
|
||||
[#1129](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1129)
|
||||
- Update client code
|
||||
[#1130](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1130)
|
||||
- Don't use fmt.Sprintf where not necessary.
|
||||
[#1131](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1131)
|
||||
- Use test-related logger for embedded etcd.
|
||||
[#1132](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1132)
|
||||
- No need to use list of pointers, use objects directly.
|
||||
[#1133](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1133)
|
||||
- Generate shorter session ids.
|
||||
[#1140](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1140)
|
||||
- client: Include version, optimize JSON processing.
|
||||
[#1143](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1143)
|
||||
- Enable more linters
|
||||
[#1145](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1145)
|
||||
- Parallelize more tests.
|
||||
[#1149](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1149)
|
||||
- Move logging code to separate package.
|
||||
[#1150](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1150)
|
||||
- Close subscriber synchronously on errors.
|
||||
[#1152](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1152)
|
||||
- CI: Run "modernize" with Go 1.25
|
||||
[#1160](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1160)
|
||||
- CI: Always use latest patch release for govuln checks.
|
||||
[#1161](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1161)
|
||||
- Process all NATS messages for same target from single goroutine.
|
||||
[#1165](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1165)
|
||||
- CI: Process files in all folders with licensecheck.
|
||||
[#1169](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1169)
|
||||
- CI: Run checklocks with Go 1.25
|
||||
[#1170](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1170)
|
||||
- Refactor code into packages
|
||||
[#1151](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1151)
|
||||
- Remove unused testing code.
|
||||
[#1174](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1174)
|
||||
- checklocks: Remove ignore since generics are supported now.
|
||||
[#1184](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1184)
|
||||
- Support receiving and forwarding multiple chat messages from Talk.
|
||||
[#1185](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1185)
|
||||
- Move tests closer to code being checked
|
||||
[#1186](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1186)
|
||||
|
||||
### Fixed
|
||||
- A proxy connection is only connected after a hello has been processed.
|
||||
[#1071](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1071)
|
||||
- Fix URL to send federated ping requests.
|
||||
[#1081](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1081)
|
||||
- Reconnect proxy connection even if shutdown was scheduled before.
|
||||
[#1100](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1100)
|
||||
- Federation cleanup fixes.
|
||||
[#1105](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1105)
|
||||
- Also rewrite token in comment for federated chat relay.
|
||||
[#1112](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1112)
|
||||
- Fix transient data for clustered setups.
|
||||
[#1121](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1121)
|
||||
- Fix initial transient data in clustered setups
|
||||
[#1127](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1127)
|
||||
- fix(docs): already_joined error response
|
||||
[#1126](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1126)
|
||||
- Fix storing initial data when clustered.
|
||||
[#1128](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1128)
|
||||
- Fix flaky tests that fail under load.
|
||||
[#1153](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1153)
|
||||
|
||||
### Dependencies
|
||||
- Bump google.golang.org/grpc from 1.74.2 to 1.75.0
|
||||
[#1054](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1054)
|
||||
- Bump github.com/nats-io/nats.go from 1.44.0 to 1.45.0
|
||||
[#1057](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1057)
|
||||
- Bump google.golang.org/protobuf from 1.36.7 to 1.36.8
|
||||
[#1056](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1056)
|
||||
- Bump github.com/stretchr/testify from 1.10.0 to 1.11.0
|
||||
[#1059](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1059)
|
||||
- Bump github.com/stretchr/testify from 1.11.0 to 1.11.1
|
||||
[#1060](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1060)
|
||||
- Bump markdown from 3.8.2 to 3.9 in /docs
|
||||
[#1065](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1065)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.15 to 3.0.16
|
||||
[#1061](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1061)
|
||||
- Bump github.com/prometheus/client_golang from 1.23.0 to 1.23.2
|
||||
[#1064](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1064)
|
||||
- Bump actions/setup-go from 5 to 6
|
||||
[#1062](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1062)
|
||||
- Bump google.golang.org/grpc from 1.75.0 to 1.75.1
|
||||
[#1070](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1070)
|
||||
- Bump google.golang.org/protobuf from 1.36.8 to 1.36.9
|
||||
[#1069](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1069)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.8 to 2.11.9
|
||||
[#1068](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1068)
|
||||
- Bump github.com/mailru/easyjson from 0.9.0 to 0.9.1
|
||||
[#1072](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1072)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1073](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1073)
|
||||
- Bump github.com/nats-io/nats.go from 1.45.0 to 1.46.0
|
||||
[#1076](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1076)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.9 to 2.12.0
|
||||
[#1075](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1075)
|
||||
- Bump github.com/nats-io/nats.go from 1.46.0 to 1.46.1
|
||||
[#1087](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1087)
|
||||
- Bump google.golang.org/grpc from 1.75.1 to 1.76.0
|
||||
[#1092](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1092)
|
||||
- Bump github/codeql-action from 3 to 4
|
||||
[#1093](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1093)
|
||||
- Bump peter-evans/create-or-update-comment from 4 to 5
|
||||
[#1090](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1090)
|
||||
- Bump google.golang.org/protobuf from 1.36.9 to 1.36.10
|
||||
[#1089](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1089)
|
||||
- Bump github.com/nats-io/nats.go from 1.46.1 to 1.47.0
|
||||
[#1096](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1096)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.12.0 to 2.12.1
|
||||
[#1095](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1095)
|
||||
- Bump the artifacts group with 2 updates
|
||||
[#1101](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1101)
|
||||
- Bump markdown from 3.9 to 3.10 in /docs
|
||||
[#1104](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1104)
|
||||
- Bump golangci/golangci-lint-action from 8.0.0 to 9.0.0
|
||||
[#1110](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1110)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1111](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1111)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.12.1 to 2.12.2
|
||||
[#1113](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1113)
|
||||
- Bump google.golang.org/grpc from 1.76.0 to 1.77.0
|
||||
[#1116](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1116)
|
||||
- Bump golang.org/x/crypto from 0.43.0 to 0.45.0
|
||||
[#1119](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1119)
|
||||
- Bump go.uber.org/zap from 1.27.0 to 1.27.1
|
||||
[#1123](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1123)
|
||||
- Bump actions/checkout from 5 to 6
|
||||
[#1124](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1124)
|
||||
- Bump golangci/golangci-lint-action from 9.0.0 to 9.1.0
|
||||
[#1125](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1125)
|
||||
- Bump github.com/pion/ice/v4 from 4.0.10 to 4.0.11
|
||||
[#1135](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1135)
|
||||
- Bump google.golang.org/grpc/cmd/protoc-gen-go-grpc from 1.5.1 to 1.6.0
|
||||
[#1136](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1136)
|
||||
- Bump github.com/pion/ice/v4 from 4.0.11 to 4.0.12
|
||||
[#1138](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1138)
|
||||
- Bump golangci/golangci-lint-action from 9.1.0 to 9.2.0
|
||||
[#1144](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1144)
|
||||
- Bump github.com/pion/ice/v4 from 4.0.12 to 4.0.13
|
||||
[#1146](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1146)
|
||||
- Bump actions/cache from 4 to 5
|
||||
[#1157](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1157)
|
||||
- Bump google.golang.org/protobuf from 1.36.10 to 1.36.11
|
||||
[#1156](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1156)
|
||||
- Bump github.com/pion/ice/v4 from 4.0.13 to 4.1.0
|
||||
[#1155](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1155)
|
||||
- Bump the artifacts group with 2 updates
|
||||
[#1154](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1154)
|
||||
- Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0
|
||||
[#1163](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1163)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1162](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1162)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.12.2 to 2.12.3
|
||||
[#1164](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1164)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.16 to 3.0.17
|
||||
[#1166](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1166)
|
||||
- Bump google.golang.org/grpc from 1.77.0 to 1.78.0
|
||||
[#1167](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1167)
|
||||
- Bump github.com/pion/ice/v4 from 4.1.0 to 4.2.0
|
||||
[#1171](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1171)
|
||||
- Bump sphinx-rtd-theme from 3.0.2 to 3.1.0 in /docs
|
||||
[#1172](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1172)
|
||||
- Bump sphinx from 8.2.3 to 9.1.0 in /docs
|
||||
[#1168](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1168)
|
||||
- Bump markdown from 3.10 to 3.10.1 in /docs
|
||||
[#1177](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1177)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.12.3 to 2.12.4
|
||||
[#1179](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1179)
|
||||
- Bump github.com/golang-jwt/jwt/v5 from 5.3.0 to 5.3.1
|
||||
[#1182](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1182)
|
||||
|
||||
|
||||
## 2.0.4 - 2025-08-18
|
||||
|
||||
### Added
|
||||
- Comment / document possible error responses.
|
||||
[#1004](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1004)
|
||||
- Support multiple sessions for dialout.
|
||||
[#1005](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1005)
|
||||
- Support filtering candidates received by clients.
|
||||
[#1000](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1000)
|
||||
- Describe how to pass caller information for outgoing calls.
|
||||
[#1019](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1019)
|
||||
- Support multiple urls per backend
|
||||
[#770](https://github.com/strukturag/nextcloud-spreed-signaling/pull/770)
|
||||
- Return connection / publisher tokens for remote publishers.
|
||||
[#1025](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1025)
|
||||
|
||||
### Changed
|
||||
- Drop support for Go 1.23
|
||||
[#1049](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1049)
|
||||
- Only forward actor id / -type in "addsession" request if both are given.
|
||||
[#1009](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1009)
|
||||
- Use backend id in backend client stats to match other stats.
|
||||
[#1020](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1020)
|
||||
- Remove debug output.
|
||||
[#1022](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1022)
|
||||
- Only forward actor details in leave virtual sessions request if both are given.
|
||||
[#1026](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1026)
|
||||
- Delete (unused) proxy publisher/subscriber created after local timeout.
|
||||
[#1032](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1032)
|
||||
- modernize: Replace "interface{}" with "any".
|
||||
[#1033](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1033)
|
||||
- Add type for string maps.
|
||||
[#1034](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1034)
|
||||
- CI: Migrate to codecov.
|
||||
[#1037](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1037)
|
||||
[#1038](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1038)
|
||||
- Use testify assertions to check expected fields / values internally.
|
||||
[#1035](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1035)
|
||||
- CI: Test with Golang 1.25
|
||||
[#1048](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1048)
|
||||
- Modernize Go code and check from CI.
|
||||
[#1050](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1050)
|
||||
- Test "HasAnyPermission" method.
|
||||
[#1051](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1051)
|
||||
- Use standard library where possible.
|
||||
[#1052](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1052)
|
||||
|
||||
### Fixed
|
||||
- Fix deadlock when setting transient data while removing listener.
|
||||
[#992](https://github.com/strukturag/nextcloud-spreed-signaling/pull/992)
|
||||
- Fixes for file watcher special cases
|
||||
[#1017](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1017)
|
||||
- Fix updating metric "signaling_mcu_subscribers" in various error cases.
|
||||
[#1027](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1027)
|
||||
|
||||
### Dependencies
|
||||
- Bump google.golang.org/grpc from 1.72.0 to 1.72.1
|
||||
[#989](https://github.com/strukturag/nextcloud-spreed-signaling/pull/989)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.11 to 3.0.12
|
||||
[#991](https://github.com/strukturag/nextcloud-spreed-signaling/pull/991)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#990](https://github.com/strukturag/nextcloud-spreed-signaling/pull/990)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.3 to 2.11.4
|
||||
[#993](https://github.com/strukturag/nextcloud-spreed-signaling/pull/993)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.12 to 3.0.13
|
||||
[#994](https://github.com/strukturag/nextcloud-spreed-signaling/pull/994)
|
||||
- Bump google.golang.org/grpc from 1.72.1 to 1.72.2
|
||||
[#995](https://github.com/strukturag/nextcloud-spreed-signaling/pull/995)
|
||||
- Bump github.com/nats-io/nats.go from 1.42.0 to 1.43.0
|
||||
[#999](https://github.com/strukturag/nextcloud-spreed-signaling/pull/999)
|
||||
- Bump google.golang.org/grpc from 1.72.2 to 1.73.0
|
||||
[#1001](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1001)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1002](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1002)
|
||||
- Bump markdown from 3.8 to 3.8.2 in /docs
|
||||
[#1008](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1008)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.13 to 3.0.14
|
||||
[#1006](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1006)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.4 to 2.11.5
|
||||
[#1010](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1010)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.5 to 2.11.6
|
||||
[#1011](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1011)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1015](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1015)
|
||||
- Bump github.com/golang-jwt/jwt/v5 from 5.2.2 to 5.2.3
|
||||
[#1016](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1016)
|
||||
- Bump google.golang.org/grpc from 1.73.0 to 1.74.0
|
||||
[#1018](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1018)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.14 to 3.0.15
|
||||
[#1021](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1021)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1023](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1023)
|
||||
- Bump google.golang.org/grpc from 1.74.0 to 1.74.2
|
||||
[#1024](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1024)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#1028](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1028)
|
||||
- Bump github.com/nats-io/nats.go from 1.43.0 to 1.44.0
|
||||
[#1031](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1031)
|
||||
- Bump github.com/golang-jwt/jwt/v5 from 5.2.3 to 5.3.0
|
||||
[#1036](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1036)
|
||||
- Bump github.com/prometheus/client_golang from 1.22.0 to 1.23.0
|
||||
[#1039](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1039)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.6 to 2.11.7
|
||||
[#1040](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1040)
|
||||
- Bump google.golang.org/protobuf from 1.36.6 to 1.36.7
|
||||
[#1043](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1043)
|
||||
- Bump actions/checkout from 4 to 5
|
||||
[#1044](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1044)
|
||||
- Bump actions/download-artifact from 4 to 5 in the artifacts group
|
||||
[#1042](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1042)
|
||||
- Bump golang from 1.24-alpine to 1.25-alpine in /docker/server
|
||||
[#1047](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1047)
|
||||
- Bump golang from 1.24-alpine to 1.25-alpine in /docker/proxy
|
||||
[#1046](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1046)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.7 to 2.11.8
|
||||
[#1053](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1053)
|
||||
|
||||
|
||||
## 2.0.3 - 2025-05-07
|
||||
|
||||
### Added
|
||||
- Allow using environment variables for sessions and clients secrets
|
||||
[#910](https://github.com/strukturag/nextcloud-spreed-signaling/pull/910)
|
||||
- Allow using environment variables for backend secrets
|
||||
[#912](https://github.com/strukturag/nextcloud-spreed-signaling/pull/912)
|
||||
- Add serverinfo API
|
||||
[#937](https://github.com/strukturag/nextcloud-spreed-signaling/pull/937)
|
||||
- Add metrics for backend client requests.
|
||||
[#973](https://github.com/strukturag/nextcloud-spreed-signaling/pull/973)
|
||||
|
||||
### Changed
|
||||
- Drop support for Go 1.22
|
||||
[#969](https://github.com/strukturag/nextcloud-spreed-signaling/pull/969)
|
||||
- Do not log nats url credentials
|
||||
[#911](https://github.com/strukturag/nextcloud-spreed-signaling/pull/911)
|
||||
- Migrate cache-control parsing to https://github.com/pquerna/cachecontrol
|
||||
[#916](https://github.com/strukturag/nextcloud-spreed-signaling/pull/916)
|
||||
- CI: Test with Golang 1.24
|
||||
[#922](https://github.com/strukturag/nextcloud-spreed-signaling/pull/922)
|
||||
- Add "/usr/lib64" to systemd ExecPath
|
||||
[#963](https://github.com/strukturag/nextcloud-spreed-signaling/pull/963)
|
||||
- Improve memory allocations
|
||||
[#870](https://github.com/strukturag/nextcloud-spreed-signaling/pull/870)
|
||||
- Speedup tests
|
||||
[#972](https://github.com/strukturag/nextcloud-spreed-signaling/pull/972)
|
||||
- docker: Make more settings configurable
|
||||
[#980](https://github.com/strukturag/nextcloud-spreed-signaling/pull/980)
|
||||
- Add jitter to reconnect intervals.
|
||||
[#988](https://github.com/strukturag/nextcloud-spreed-signaling/pull/988)
|
||||
|
||||
### Fixed
|
||||
- nats: Reconnect client indefinitely.
|
||||
[#935](https://github.com/strukturag/nextcloud-spreed-signaling/pull/935)
|
||||
- Explicitly set TMPDIR to ensure that it is an executable path
|
||||
[#956](https://github.com/strukturag/nextcloud-spreed-signaling/pull/956)
|
||||
- Close subscribers on errors during initial connection.
|
||||
[#959](https://github.com/strukturag/nextcloud-spreed-signaling/pull/959)
|
||||
- Fix formatting of errors in "assert.Fail" calls.
|
||||
[#970](https://github.com/strukturag/nextcloud-spreed-signaling/pull/970)
|
||||
- Fix race condition in flaky certificate/CA reload tests.
|
||||
[#971](https://github.com/strukturag/nextcloud-spreed-signaling/pull/971)
|
||||
- Fix flaky test "Test_GrpcClients_DnsDiscovery".
|
||||
[#976](https://github.com/strukturag/nextcloud-spreed-signaling/pull/976)
|
||||
- Fix subscribers not closed when publisher is closed in Janus 1.x
|
||||
[#986](https://github.com/strukturag/nextcloud-spreed-signaling/pull/986)
|
||||
- Close subscriber if remote publisher was closed.
|
||||
[#987](https://github.com/strukturag/nextcloud-spreed-signaling/pull/987)
|
||||
|
||||
### Dependencies
|
||||
- Bump google.golang.org/grpc from 1.69.4 to 1.70.0
|
||||
[#904](https://github.com/strukturag/nextcloud-spreed-signaling/pull/904)
|
||||
- Bump the etcd group with 4 updates
|
||||
[#907](https://github.com/strukturag/nextcloud-spreed-signaling/pull/907)
|
||||
- Bump coverallsapp/github-action from 2.3.4 to 2.3.6
|
||||
[#909](https://github.com/strukturag/nextcloud-spreed-signaling/pull/909)
|
||||
- Bump google.golang.org/protobuf from 1.36.3 to 1.36.4
|
||||
[#908](https://github.com/strukturag/nextcloud-spreed-signaling/pull/908)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.10.24 to 2.10.25
|
||||
[#903](https://github.com/strukturag/nextcloud-spreed-signaling/pull/903)
|
||||
- Bump golangci/golangci-lint-action from 6.2.0 to 6.3.0
|
||||
[#913](https://github.com/strukturag/nextcloud-spreed-signaling/pull/913)
|
||||
- Bump github.com/nats-io/nats.go from 1.38.0 to 1.39.0
|
||||
[#915](https://github.com/strukturag/nextcloud-spreed-signaling/pull/915)
|
||||
- Bump golangci/golangci-lint-action from 6.3.0 to 6.3.2
|
||||
[#918](https://github.com/strukturag/nextcloud-spreed-signaling/pull/918)
|
||||
- Bump google.golang.org/protobuf from 1.36.4 to 1.36.5
|
||||
[#917](https://github.com/strukturag/nextcloud-spreed-signaling/pull/917)
|
||||
- build(deps): bump golang from 1.23-alpine to 1.24-alpine in /docker/proxy
|
||||
[#921](https://github.com/strukturag/nextcloud-spreed-signaling/pull/921)
|
||||
- build(deps): bump golang from 1.23-alpine to 1.24-alpine in /docker/server
|
||||
[#919](https://github.com/strukturag/nextcloud-spreed-signaling/pull/919)
|
||||
- build(deps): bump sphinx from 8.1.3 to 8.2.0 in /docs
|
||||
[#928](https://github.com/strukturag/nextcloud-spreed-signaling/pull/928)
|
||||
- build(deps): bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0
|
||||
[#927](https://github.com/strukturag/nextcloud-spreed-signaling/pull/927)
|
||||
- build(deps): bump sphinx from 8.2.0 to 8.2.1 in /docs
|
||||
[#929](https://github.com/strukturag/nextcloud-spreed-signaling/pull/929)
|
||||
- build(deps): bump github.com/nats-io/nats.go from 1.39.0 to 1.39.1
|
||||
[#926](https://github.com/strukturag/nextcloud-spreed-signaling/pull/926)
|
||||
- build(deps): bump sphinx from 8.2.1 to 8.2.3 in /docs
|
||||
[#932](https://github.com/strukturag/nextcloud-spreed-signaling/pull/932)
|
||||
- build(deps): bump google.golang.org/grpc from 1.70.0 to 1.71.0
|
||||
[#933](https://github.com/strukturag/nextcloud-spreed-signaling/pull/933)
|
||||
- build(deps): bump golangci/golangci-lint-action from 6.3.3 to 6.5.0
|
||||
[#925](https://github.com/strukturag/nextcloud-spreed-signaling/pull/925)
|
||||
- build(deps): bump jinja2 from 3.1.5 to 3.1.6 in /docs
|
||||
[#938](https://github.com/strukturag/nextcloud-spreed-signaling/pull/938)
|
||||
- build(deps): bump github.com/pion/sdp/v3 from 3.0.10 to 3.0.11
|
||||
[#939](https://github.com/strukturag/nextcloud-spreed-signaling/pull/939)
|
||||
- build(deps): bump golangci/golangci-lint-action from 6.5.0 to 6.5.1
|
||||
[#940](https://github.com/strukturag/nextcloud-spreed-signaling/pull/940)
|
||||
- build(deps): bump golangci/golangci-lint-action from 6.5.1 to 6.5.2
|
||||
[#946](https://github.com/strukturag/nextcloud-spreed-signaling/pull/946)
|
||||
- build(deps): bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2
|
||||
[#948](https://github.com/strukturag/nextcloud-spreed-signaling/pull/948)
|
||||
- build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2
|
||||
[#949](https://github.com/strukturag/nextcloud-spreed-signaling/pull/949)
|
||||
- build(deps): bump golangci/golangci-lint-action from 6.5.2 to 7.0.0
|
||||
[#951](https://github.com/strukturag/nextcloud-spreed-signaling/pull/951)
|
||||
- build(deps): bump google.golang.org/protobuf from 1.36.5 to 1.36.6
|
||||
[#952](https://github.com/strukturag/nextcloud-spreed-signaling/pull/952)
|
||||
- Bump markdown from 3.7 to 3.8 in /docs
|
||||
[#966](https://github.com/strukturag/nextcloud-spreed-signaling/pull/966)
|
||||
- Bump golang.org/x/crypto from 0.32.0 to 0.35.0
|
||||
[#967](https://github.com/strukturag/nextcloud-spreed-signaling/pull/967)
|
||||
- Bump github.com/nats-io/nats.go from 1.39.1 to 1.41.1
|
||||
[#964](https://github.com/strukturag/nextcloud-spreed-signaling/pull/964)
|
||||
- Bump google.golang.org/grpc from 1.71.0 to 1.71.1
|
||||
[#957](https://github.com/strukturag/nextcloud-spreed-signaling/pull/957)
|
||||
- build(deps): bump golang.org/x/net from 0.34.0 to 0.36.0
|
||||
[#941](https://github.com/strukturag/nextcloud-spreed-signaling/pull/941)
|
||||
- build(deps): bump the etcd group with 4 updates
|
||||
[#936](https://github.com/strukturag/nextcloud-spreed-signaling/pull/936)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.10.25 to 2.11.1
|
||||
[#962](https://github.com/strukturag/nextcloud-spreed-signaling/pull/962)
|
||||
- Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0
|
||||
[#974](https://github.com/strukturag/nextcloud-spreed-signaling/pull/974)
|
||||
- Bump github.com/fsnotify/fsnotify from 1.8.0 to 1.9.0
|
||||
[#975](https://github.com/strukturag/nextcloud-spreed-signaling/pull/975)
|
||||
- Bump google.golang.org/grpc from 1.71.1 to 1.72.0
|
||||
[#978](https://github.com/strukturag/nextcloud-spreed-signaling/pull/978)
|
||||
- Bump github.com/nats-io/nats.go from 1.41.1 to 1.41.2
|
||||
[#977](https://github.com/strukturag/nextcloud-spreed-signaling/pull/977)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.11.1 to 2.11.3
|
||||
[#982](https://github.com/strukturag/nextcloud-spreed-signaling/pull/982)
|
||||
- Bump github.com/nats-io/nats.go from 1.41.2 to 1.42.0
|
||||
[#983](https://github.com/strukturag/nextcloud-spreed-signaling/pull/983)
|
||||
- Bump golangci/golangci-lint-action from 7.0.0 to 8.0.0
|
||||
[#985](https://github.com/strukturag/nextcloud-spreed-signaling/pull/985)
|
||||
|
||||
|
||||
## 2.0.2 - 2025-01-22
|
||||
|
||||
### Added
|
||||
|
|
|
|||
85
Makefile
85
Makefile
|
|
@ -6,28 +6,28 @@ GODIR := $(shell dirname "$(GO)")
|
|||
GOFMT := "$(GODIR)/gofmt"
|
||||
GOOS ?= linux
|
||||
GOARCH ?= amd64
|
||||
GOVERSION := $(shell "$(GO)" env GOVERSION | sed "s|go||" )
|
||||
GOVERSION := $(shell "$(GO)" env GOVERSION | sed -E 's|go([0-9]+\.[0-9]+)\..*|\1|')
|
||||
TMPDIR := $(CURDIR)/tmp
|
||||
BINDIR := $(CURDIR)/bin
|
||||
VENDORDIR := "$(CURDIR)/vendor"
|
||||
VERSION := $(shell "$(CURDIR)/scripts/get-version.sh")
|
||||
TARVERSION := $(shell "$(CURDIR)/scripts/get-version.sh" --tar)
|
||||
PACKAGENAME := github.com/strukturag/nextcloud-spreed-signaling
|
||||
ALL_PACKAGES := $(PACKAGENAME) $(PACKAGENAME)/client $(PACKAGENAME)/proxy $(PACKAGENAME)/server
|
||||
GRPC_PROTO_FILES := $(basename $(wildcard grpc_*.proto))
|
||||
GRPC_PROTO_FILES := $(basename $(wildcard grpc/*.proto))
|
||||
PROTOBUF_VERSION := $(shell grep google.golang.org/protobuf go.mod | xargs | cut -d ' ' -f 2)
|
||||
PROTO_FILES := $(filter-out $(GRPC_PROTO_FILES),$(basename $(wildcard *.proto)))
|
||||
PROTO_FILES := $(filter-out $(GRPC_PROTO_FILES),$(basename $(wildcard *.proto */*.proto)))
|
||||
PROTO_GO_FILES := $(addsuffix .pb.go,$(PROTO_FILES))
|
||||
GRPC_PROTO_GO_FILES := $(addsuffix .pb.go,$(GRPC_PROTO_FILES)) $(addsuffix _grpc.pb.go,$(GRPC_PROTO_FILES))
|
||||
TEST_GO_FILES := $(wildcard *_test.go))
|
||||
EASYJSON_FILES := $(filter-out $(TEST_GO_FILES),$(wildcard api*.go))
|
||||
TEST_GO_FILES := $(wildcard *_test.go */*_test.go */*/*_test.go)
|
||||
EASYJSON_FILES := $(filter-out $(TEST_GO_FILES),$(wildcard api*.go api/signaling.go */api.go */*/api.go talk/ocs.go))
|
||||
EASYJSON_GO_FILES := $(patsubst %.go,%_easyjson.go,$(EASYJSON_FILES))
|
||||
COMMON_GO_FILES := $(filter-out continentmap.go $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) $(EASYJSON_GO_FILES) $(TEST_GO_FILES),$(wildcard *.go))
|
||||
CLIENT_TEST_GO_FILES := $(wildcard client/*_test.go))
|
||||
CLIENT_GO_FILES := $(filter-out $(CLIENT_TEST_GO_FILES),$(wildcard client/*.go))
|
||||
SERVER_TEST_GO_FILES := $(wildcard server/*_test.go))
|
||||
SERVER_GO_FILES := $(filter-out $(SERVER_TEST_GO_FILES),$(wildcard server/*.go))
|
||||
PROXY_TEST_GO_FILES := $(wildcard proxy/*_test.go))
|
||||
PROXY_GO_FILES := $(filter-out $(PROXY_TEST_GO_FILES),$(wildcard proxy/*.go))
|
||||
COMMON_GO_FILES := $(filter-out geoip/continentmap.go $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) $(EASYJSON_GO_FILES) $(TEST_GO_FILES),$(wildcard *.go */*.go */*/*.go))
|
||||
CLIENT_TEST_GO_FILES := $(wildcard cmd/client/*_test.go))
|
||||
CLIENT_GO_FILES := $(filter-out $(CLIENT_TEST_GO_FILES),$(wildcard cmd/client/*.go))
|
||||
SERVER_TEST_GO_FILES := $(wildcard cmd/server/*_test.go))
|
||||
SERVER_GO_FILES := $(filter-out $(SERVER_TEST_GO_FILES),$(wildcard cmd/server/*.go))
|
||||
PROXY_TEST_GO_FILES := $(wildcard cmd/proxy/*_test.go))
|
||||
PROXY_GO_FILES := $(filter-out $(PROXY_TEST_GO_FILES),$(wildcard cmd/proxy/*.go))
|
||||
|
||||
ifneq ($(VERSION),)
|
||||
INTERNALLDFLAGS := -X main.version=$(VERSION)
|
||||
|
|
@ -51,6 +51,10 @@ ifeq ($(TIMEOUT),)
|
|||
TIMEOUT := 60s
|
||||
endif
|
||||
|
||||
ifeq ($(BENCHMARK),)
|
||||
BENCHMARK := .
|
||||
endif
|
||||
|
||||
ifneq ($(TEST),)
|
||||
TESTARGS := $(TESTARGS) -run "$(TEST)"
|
||||
endif
|
||||
|
|
@ -73,10 +77,12 @@ else
|
|||
GOPATHBIN := $(GOPATH)/bin/$(GOOS)_$(GOARCH)
|
||||
endif
|
||||
|
||||
GOEXPERIMENT :=
|
||||
|
||||
hook:
|
||||
[ ! -d "$(CURDIR)/.git/hooks" ] || ln -sf "$(CURDIR)/scripts/pre-commit.hook" "$(CURDIR)/.git/hooks/pre-commit"
|
||||
|
||||
$(GOPATHBIN)/easyjson: go.mod go.sum
|
||||
$(GOPATHBIN)/easyjson: go.mod go.sum | $(TMPDIR)
|
||||
$(GO) install github.com/mailru/easyjson/...
|
||||
|
||||
$(GOPATHBIN)/protoc-gen-go: go.mod go.sum
|
||||
|
|
@ -85,7 +91,10 @@ $(GOPATHBIN)/protoc-gen-go: go.mod go.sum
|
|||
$(GOPATHBIN)/protoc-gen-go-grpc: go.mod go.sum
|
||||
$(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
||||
|
||||
continentmap.go:
|
||||
$(GOPATHBIN)/checklocks: go.mod go.sum
|
||||
$(GO) install gvisor.dev/gvisor/tools/checklocks/cmd/checklocks@go
|
||||
|
||||
geoip/continentmap.go:
|
||||
$(CURDIR)/scripts/get_continent_map.py $@
|
||||
|
||||
check-continentmap:
|
||||
|
|
@ -93,38 +102,41 @@ check-continentmap:
|
|||
TMP=$$(mktemp -d) ;\
|
||||
echo Make sure to remove $$TMP on error ;\
|
||||
$(CURDIR)/scripts/get_continent_map.py $$TMP/continentmap.go ;\
|
||||
diff -u continentmap.go $$TMP/continentmap.go ;\
|
||||
diff -u geoip/continentmap.go $$TMP/continentmap.go ;\
|
||||
rm -rf $$TMP
|
||||
|
||||
get:
|
||||
$(GO) get $(PACKAGE)
|
||||
|
||||
fmt: hook | $(PROTO_GO_FILES)
|
||||
$(GOFMT) -s -w *.go client proxy server
|
||||
$(GOFMT) -s -w *.go cmd/client cmd/proxy cmd/server
|
||||
|
||||
vet:
|
||||
$(GO) vet $(ALL_PACKAGES)
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) $(GO) vet ./...
|
||||
|
||||
test: vet
|
||||
$(GO) test -timeout $(TIMEOUT) $(TESTARGS) $(ALL_PACKAGES)
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -timeout $(TIMEOUT) $(TESTARGS) ./...
|
||||
|
||||
benchmark:
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -bench=$(BENCHMARK) -benchmem -run=^$$ -timeout $(TIMEOUT) $(TESTARGS) ./...
|
||||
|
||||
checklocks: $(GOPATHBIN)/checklocks
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) $(GOPATHBIN)/checklocks ./...
|
||||
|
||||
cover: vet
|
||||
rm -f cover.out && \
|
||||
$(GO) test -timeout $(TIMEOUT) -coverprofile cover.out $(ALL_PACKAGES) && \
|
||||
sed -i "/_easyjson/d" cover.out && \
|
||||
sed -i "/\.pb\.go/d" cover.out && \
|
||||
$(GO) tool cover -func=cover.out
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -timeout $(TIMEOUT) -coverprofile cover.out ./...
|
||||
|
||||
coverhtml: vet
|
||||
rm -f cover.out && \
|
||||
$(GO) test -timeout $(TIMEOUT) -coverprofile cover.out $(ALL_PACKAGES) && \
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -timeout $(TIMEOUT) -coverprofile cover.out ./... && \
|
||||
sed -i "/_easyjson/d" cover.out && \
|
||||
sed -i "/\.pb\.go/d" cover.out && \
|
||||
$(GO) tool cover -html=cover.out -o coverage.html
|
||||
|
||||
%_easyjson.go: %.go $(GOPATHBIN)/easyjson | $(PROTO_GO_FILES)
|
||||
rm -f easyjson-bootstrap*.go
|
||||
PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $*.go
|
||||
TMPDIR=$(TMPDIR) PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $*.go
|
||||
|
||||
%.pb.go: %.proto $(GOPATHBIN)/protoc-gen-go $(GOPATHBIN)/protoc-gen-go-grpc
|
||||
PATH="$(GODIR)":"$(GOPATHBIN)":$(PATH) protoc \
|
||||
|
|
@ -142,32 +154,37 @@ common: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES)
|
|||
# Optimize easyjson files that could call generated functions instead of duplicating code.
|
||||
for file in $(EASYJSON_FILES); do \
|
||||
rm -f easyjson-bootstrap*.go; \
|
||||
PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $$file; \
|
||||
TMPDIR=$(TMPDIR) PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $$file; \
|
||||
rm -f *_easyjson_easyjson.go; \
|
||||
done
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p "$(BINDIR)"
|
||||
|
||||
$(TMPDIR):
|
||||
mkdir -p "$(TMPDIR)"
|
||||
|
||||
client: $(BINDIR)/client
|
||||
|
||||
$(BINDIR)/client: go.mod go.sum $(CLIENT_GO_FILES) $(COMMON_GO_FILES) | $(BINDIR)
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./client/...
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./cmd/client/...
|
||||
|
||||
server: $(BINDIR)/signaling
|
||||
|
||||
$(BINDIR)/signaling: go.mod go.sum $(SERVER_GO_FILES) $(COMMON_GO_FILES) | $(BINDIR)
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./server/...
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./cmd/server/...
|
||||
|
||||
proxy: $(BINDIR)/proxy
|
||||
|
||||
$(BINDIR)/proxy: go.mod go.sum $(PROXY_GO_FILES) $(COMMON_GO_FILES) | $(BINDIR)
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./proxy/...
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./cmd/proxy/...
|
||||
|
||||
clean:
|
||||
rm -f easyjson-bootstrap*.go
|
||||
rm -f "$(BINDIR)/client"
|
||||
rm -f "$(BINDIR)/signaling"
|
||||
rm -f "$(BINDIR)/proxy"
|
||||
rm -rf "$(TMPDIR)"
|
||||
|
||||
clean-generated: clean
|
||||
rm -f $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES)
|
||||
|
|
@ -179,7 +196,7 @@ vendor: go.mod go.sum
|
|||
rm -rf $(VENDORDIR)
|
||||
$(GO) mod vendor
|
||||
|
||||
tarball: vendor
|
||||
tarball: vendor | $(TMPDIR)
|
||||
git archive \
|
||||
--prefix=nextcloud-spreed-signaling-$(TARVERSION)/ \
|
||||
-o nextcloud-spreed-signaling-$(TARVERSION).tar \
|
||||
|
|
@ -189,11 +206,17 @@ tarball: vendor
|
|||
--mtime="$(shell git log -1 --date=iso8601-strict --format=%cd HEAD)" \
|
||||
--transform "s//nextcloud-spreed-signaling-$(TARVERSION)\//" \
|
||||
vendor
|
||||
echo "$(TARVERSION)" > "$(TMPDIR)/version.txt"
|
||||
tar rf nextcloud-spreed-signaling-$(TARVERSION).tar \
|
||||
-C "$(TMPDIR)" \
|
||||
--mtime="$(shell git log -1 --date=iso8601-strict --format=%cd HEAD)" \
|
||||
--transform "s//nextcloud-spreed-signaling-$(TARVERSION)\//" \
|
||||
version.txt
|
||||
gzip --force nextcloud-spreed-signaling-$(TARVERSION).tar
|
||||
|
||||
dist: tarball
|
||||
|
||||
.NOTPARALLEL: $(EASYJSON_GO_FILES)
|
||||
.PHONY: continentmap.go common vendor
|
||||
.PHONY: geoip/continentmap.go common vendor
|
||||
.SECONDARY: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES)
|
||||
.DELETE_ON_ERROR:
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# Spreed standalone signaling server
|
||||
|
||||

|
||||
[](https://coveralls.io/github/strukturag/nextcloud-spreed-signaling?branch=master)
|
||||
[](https://codecov.io/gh/strukturag/nextcloud-spreed-signaling)
|
||||
[](https://nextcloud-spreed-signaling.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://goreportcard.com/report/github.com/strukturag/nextcloud-spreed-signaling)
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ information on the API of the signaling server.
|
|||
The following tools are required for building the signaling server.
|
||||
|
||||
- git
|
||||
- go >= 1.22
|
||||
- go >= 1.25
|
||||
- make
|
||||
|
||||
Usually the last two versions of Go are supported. This follows the release
|
||||
|
|
@ -94,11 +94,19 @@ systemctl start signaling.service
|
|||
|
||||
### Running with Docker
|
||||
|
||||
Official docker containers for the signaling server and -proxy are available on
|
||||
Official docker images for the signaling server and -proxy are available on
|
||||
Docker Hub at https://hub.docker.com/r/strukturag/nextcloud-spreed-signaling
|
||||
|
||||
See the `README.md` in the `docker` subfolder for details.
|
||||
See the `README.md` in the `docker` subfolder for details on how to use and
|
||||
configure them.
|
||||
|
||||
To build the images locally, run the following commands (replace the parameter
|
||||
after `-t` with the name the image should be tagged as):
|
||||
|
||||
```bash
|
||||
docker build -f docker/server/Dockerfile -t nextcloud-spreed-signaling .
|
||||
docker build -f docker/proxy/Dockerfile -t nextcloud-spreed-signaling-proxy .
|
||||
```
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
|
|
@ -131,14 +139,30 @@ server.
|
|||
|
||||
A Janus server (from https://github.com/meetecho/janus-gateway) can be used to
|
||||
act as a WebRTC gateway. See the documentation of Janus on how to configure and
|
||||
run the server. At least the `VideoRoom` plugin and the websocket transport of
|
||||
Janus must be enabled.
|
||||
run the server. At least the `VideoRoom` plugin, the websocket transport and the
|
||||
websocket events handler of Janus must be enabled. Also broadcasting of events
|
||||
must be enabled.
|
||||
|
||||
The signaling server uses the `VideoRoom` plugin of Janus to manage sessions.
|
||||
All gateway details are hidden from the clients, all messages are sent through
|
||||
the signaling server. Only WebRTC media is exchanged directly between the
|
||||
gateway and the clients.
|
||||
|
||||
To enable sending of events from Janus, the option `broadcast` must be set to
|
||||
`true` in the block `events` of `janus.jcfg`. In the configuration of the
|
||||
websocket events handler (`janus.eventhandler.wsevh.jcfg`), the module must be
|
||||
enabled by setting `enabled` to `true`, the `backend` must be set to the
|
||||
websocket url of the signaling server (`ws://127.0.0.1:port/spreed`) or -proxy
|
||||
(`ws://127.0.0.1:port/proxy`) and `subprotocol` must be set to `janus-events`.
|
||||
At least events of type `handles`, `media` and `webrtc` must be subscribed.
|
||||
|
||||
Warning: If the configuration between Janus and the signaling endpoint is
|
||||
interrupted or can't be established, unsent events will be queued by Janus
|
||||
and will use potentially lots of memory there. This can be limited by setting
|
||||
`events_cap_on_reconnect` in `janus.eventhandler.wsevh.jcfg`. By default, all
|
||||
events will be queued as the connection between Janus and the signaling endpoint
|
||||
is assumed to be stable (most likely will be on the same machine).
|
||||
|
||||
Edit the `server.conf` and enter the URL to the websocket endpoint of Janus in
|
||||
the section `[mcu]` and key `url`. During startup, the signaling server will
|
||||
connect to Janus and log information of the gateway.
|
||||
|
|
|
|||
111
api/bandwidth.go
Normal file
111
api/bandwidth.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
Kilobit = BandwidthFromBits(1024)
|
||||
Megabit = BandwidthFromBits(1024) * Kilobit
|
||||
Gigabit = BandwidthFromBits(1024) * Megabit
|
||||
)
|
||||
|
||||
// Bandwidth stores a bandwidth in bits per second.
|
||||
type Bandwidth uint64
|
||||
|
||||
func formatWithRemainder(value uint64, divisor uint64, format string) string {
|
||||
if value%divisor == 0 {
|
||||
return fmt.Sprintf("%d %s", value/divisor, format)
|
||||
} else {
|
||||
v := float64(value) / float64(divisor)
|
||||
v = math.Trunc(v*100) / 100
|
||||
return fmt.Sprintf("%.2f %s", v, format)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the formatted bandwidth.
|
||||
func (b Bandwidth) String() string {
|
||||
switch {
|
||||
case b >= Gigabit:
|
||||
return formatWithRemainder(b.Bits(), Gigabit.Bits(), "Gbps")
|
||||
case b >= Megabit:
|
||||
return formatWithRemainder(b.Bits(), Megabit.Bits(), "Mbps")
|
||||
case b >= Kilobit:
|
||||
return formatWithRemainder(b.Bits(), Kilobit.Bits(), "Kbps")
|
||||
default:
|
||||
return fmt.Sprintf("%d bps", b)
|
||||
}
|
||||
}
|
||||
|
||||
// Bits returns the bandwidth in bits per second.
|
||||
func (b Bandwidth) Bits() uint64 {
|
||||
return uint64(b)
|
||||
}
|
||||
|
||||
// Bytes returns the bandwidth in bytes per second.
|
||||
func (b Bandwidth) Bytes() uint64 {
|
||||
return b.Bits() / 8
|
||||
}
|
||||
|
||||
// BandwidthFromBits creates a bandwidth from bits per second.
|
||||
func BandwidthFromBits(b uint64) Bandwidth {
|
||||
return Bandwidth(b)
|
||||
}
|
||||
|
||||
// BandwithFromBits creates a bandwidth from megabits per second.
|
||||
func BandwidthFromMegabits(b uint64) Bandwidth {
|
||||
return Bandwidth(b) * Megabit
|
||||
}
|
||||
|
||||
// BandwidthFromBytes creates a bandwidth from bytes per second.
|
||||
func BandwidthFromBytes(b uint64) Bandwidth {
|
||||
return Bandwidth(b * 8)
|
||||
}
|
||||
|
||||
// AtomicBandwidth is an atomic Bandwidth. The zero value is zero.
|
||||
// AtomicBandwidth must not be copied after first use.
|
||||
type AtomicBandwidth struct {
|
||||
// 64-bit members that are accessed atomically must be 64-bit aligned.
|
||||
v uint64
|
||||
_ internal.NoCopy
|
||||
}
|
||||
|
||||
// Load atomically loads and returns the value stored in b.
|
||||
func (b *AtomicBandwidth) Load() Bandwidth {
|
||||
return Bandwidth(atomic.LoadUint64(&b.v)) // +checklocksignore
|
||||
}
|
||||
|
||||
// Store atomically stores v into b.
|
||||
func (b *AtomicBandwidth) Store(v Bandwidth) {
|
||||
atomic.StoreUint64(&b.v, uint64(v)) // +checklocksignore
|
||||
}
|
||||
|
||||
// Swap atomically stores v into b and returns the previous value.
|
||||
func (b *AtomicBandwidth) Swap(v Bandwidth) Bandwidth {
|
||||
return Bandwidth(atomic.SwapUint64(&b.v, uint64(v))) // +checklocksignore
|
||||
}
|
||||
109
api/bandwidth_test.go
Normal file
109
api/bandwidth_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBandwidth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
var b Bandwidth
|
||||
assert.EqualValues(0, b.Bits())
|
||||
assert.EqualValues(0, b.Bytes())
|
||||
|
||||
b = BandwidthFromBits(8000)
|
||||
assert.EqualValues(8000, b.Bits())
|
||||
assert.EqualValues(1000, b.Bytes())
|
||||
|
||||
b = BandwidthFromBytes(1000)
|
||||
assert.EqualValues(8000, b.Bits())
|
||||
assert.EqualValues(1000, b.Bytes())
|
||||
|
||||
b = BandwidthFromMegabits(2)
|
||||
assert.EqualValues(2*1024*1024, b.Bits())
|
||||
assert.EqualValues(2*1024*1024/8, b.Bytes())
|
||||
|
||||
var a AtomicBandwidth
|
||||
assert.EqualValues(0, a.Load())
|
||||
a.Store(1000)
|
||||
assert.EqualValues(1000, a.Load())
|
||||
old := a.Swap(2000)
|
||||
assert.EqualValues(1000, old)
|
||||
assert.EqualValues(2000, a.Load())
|
||||
}
|
||||
|
||||
func TestBandwidthString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
testcases := []struct {
|
||||
value Bandwidth
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
0,
|
||||
"0 bps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(123),
|
||||
"123 bps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1023),
|
||||
"1023 bps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1024),
|
||||
"1 Kbps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1024 + 512),
|
||||
"1.50 Kbps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1024*1024 - 1),
|
||||
"1023.99 Kbps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1024 * 1024),
|
||||
"1 Mbps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1024*1024*1024 - 1),
|
||||
"1023.99 Mbps",
|
||||
},
|
||||
{
|
||||
BandwidthFromBits(1024 * 1024 * 1024),
|
||||
"1 Gbps",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testcases {
|
||||
assert.Equal(tc.expected, tc.value.String(), "failed for testcase %d (%d)", idx, tc.value.Bits())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1081
api/signaling_test.go
Normal file
1081
api/signaling_test.go
Normal file
File diff suppressed because it is too large
Load diff
78
api/stringmap.go
Normal file
78
api/stringmap.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2021 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package api
|
||||
|
||||
// StringMap maps string keys to arbitrary values.
|
||||
type StringMap map[string]any
|
||||
|
||||
func (m StringMap) GetStringMap(key string) (StringMap, bool) {
|
||||
v, found := m[key]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return ConvertStringMap(v)
|
||||
}
|
||||
|
||||
func ConvertStringMap(ob any) (StringMap, bool) {
|
||||
if ob == nil {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
switch ob := ob.(type) {
|
||||
case map[string]any:
|
||||
return StringMap(ob), true
|
||||
case StringMap:
|
||||
return ob, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// GetStringMapEntry returns an entry from a string map in a given type.
|
||||
func GetStringMapEntry[T any](m StringMap, key string) (s T, ok bool) {
|
||||
var defaultValue T
|
||||
v, found := m[key]
|
||||
if !found {
|
||||
return defaultValue, false
|
||||
}
|
||||
|
||||
s, ok = v.(T)
|
||||
return
|
||||
}
|
||||
|
||||
func GetStringMapString[T ~string](m StringMap, key string) (T, bool) {
|
||||
var defaultValue T
|
||||
v, found := m[key]
|
||||
if !found {
|
||||
return defaultValue, false
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return T(v), true
|
||||
case T:
|
||||
return v, true
|
||||
default:
|
||||
return defaultValue, false
|
||||
}
|
||||
}
|
||||
118
api/stringmap_test.go
Normal file
118
api/stringmap_test.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2021 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertStringMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
d := map[string]any{
|
||||
"foo": "bar",
|
||||
"bar": 2,
|
||||
}
|
||||
|
||||
m, ok := ConvertStringMap(d)
|
||||
if assert.True(ok) {
|
||||
assert.EqualValues(d, m)
|
||||
}
|
||||
|
||||
if m, ok := ConvertStringMap(nil); assert.True(ok) {
|
||||
assert.Nil(m)
|
||||
}
|
||||
|
||||
_, ok = ConvertStringMap("foo")
|
||||
assert.False(ok)
|
||||
|
||||
_, ok = ConvertStringMap(1)
|
||||
assert.False(ok)
|
||||
|
||||
_, ok = ConvertStringMap(map[int]any{
|
||||
1: "foo",
|
||||
})
|
||||
assert.False(ok)
|
||||
}
|
||||
|
||||
func TestGetStringMapString(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
type StringMapTestString string
|
||||
|
||||
var ok bool
|
||||
m := StringMap{
|
||||
"foo": "bar",
|
||||
"bar": StringMapTestString("baz"),
|
||||
"baz": 1234,
|
||||
}
|
||||
if v, ok := GetStringMapString[string](m, "foo"); assert.True(ok) {
|
||||
assert.Equal("bar", v)
|
||||
}
|
||||
if v, ok := GetStringMapString[StringMapTestString](m, "foo"); assert.True(ok) {
|
||||
assert.Equal(StringMapTestString("bar"), v)
|
||||
}
|
||||
v, ok := GetStringMapString[string](m, "bar")
|
||||
assert.False(ok, "should not find object, got %+v", v)
|
||||
|
||||
if v, ok := GetStringMapString[StringMapTestString](m, "bar"); assert.True(ok) {
|
||||
assert.Equal(StringMapTestString("baz"), v)
|
||||
}
|
||||
|
||||
_, ok = GetStringMapString[string](m, "baz")
|
||||
assert.False(ok)
|
||||
_, ok = GetStringMapString[StringMapTestString](m, "baz")
|
||||
assert.False(ok)
|
||||
_, ok = GetStringMapString[string](m, "invalid")
|
||||
assert.False(ok)
|
||||
_, ok = GetStringMapString[StringMapTestString](m, "invalid")
|
||||
assert.False(ok)
|
||||
}
|
||||
|
||||
func TestGetStringMapStringMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
m := StringMap{
|
||||
"foo": map[string]any{
|
||||
"bar": 1,
|
||||
},
|
||||
"bar": StringMap{
|
||||
"baz": 2,
|
||||
},
|
||||
}
|
||||
if v, ok := m.GetStringMap("foo"); assert.True(ok) {
|
||||
assert.EqualValues(map[string]any{
|
||||
"bar": 1,
|
||||
}, v)
|
||||
}
|
||||
if v, ok := m.GetStringMap("bar"); assert.True(ok) {
|
||||
assert.EqualValues(map[string]any{
|
||||
"baz": 2,
|
||||
}, v)
|
||||
}
|
||||
v, ok := m.GetStringMap("baz")
|
||||
assert.False(ok, "expected missing entry, got %+v", v)
|
||||
}
|
||||
405
api/transient_data.go
Normal file
405
api/transient_data.go
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2021 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TransientSessionDataPrefix = "sd:"
|
||||
)
|
||||
|
||||
type TransientListener interface {
|
||||
SendMessage(message *ServerMessage) bool
|
||||
}
|
||||
|
||||
type TransientDataEntry struct {
|
||||
Value any `json:"value"`
|
||||
Expires time.Time `json:"expires,omitzero"`
|
||||
}
|
||||
|
||||
func NewTransientDataEntry(value any, ttl time.Duration) *TransientDataEntry {
|
||||
entry := &TransientDataEntry{
|
||||
Value: value,
|
||||
}
|
||||
if ttl > 0 {
|
||||
entry.Expires = time.Now().Add(ttl)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func NewTransientDataEntryWithExpires(value any, expires time.Time) *TransientDataEntry {
|
||||
entry := &TransientDataEntry{
|
||||
Value: value,
|
||||
Expires: expires,
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (e *TransientDataEntry) clone() *TransientDataEntry {
|
||||
result := *e
|
||||
return &result
|
||||
}
|
||||
|
||||
func (e *TransientDataEntry) update(value any, ttl time.Duration) {
|
||||
e.Value = value
|
||||
if ttl > 0 {
|
||||
e.Expires = time.Now().Add(ttl)
|
||||
} else {
|
||||
e.Expires = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
type TransientDataEntries map[string]*TransientDataEntry
|
||||
|
||||
func (e TransientDataEntries) String() string {
|
||||
data, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Could not serialize %#v: %s", e, err)
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type TransientData struct {
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
data TransientDataEntries
|
||||
// +checklocks:mu
|
||||
listeners map[TransientListener]bool
|
||||
// +checklocks:mu
|
||||
timers map[string]*time.Timer
|
||||
}
|
||||
|
||||
// NewTransientData creates a new transient data container.
|
||||
func NewTransientData() *TransientData {
|
||||
return &TransientData{}
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) sendMessageToListener(listener TransientListener, message *ServerMessage) {
|
||||
t.mu.Unlock()
|
||||
defer t.mu.Lock()
|
||||
|
||||
listener.SendMessage(message)
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) notifySet(key string, prev, value any) {
|
||||
msg := &ServerMessage{
|
||||
Type: "transient",
|
||||
TransientData: &TransientDataServerMessage{
|
||||
Type: "set",
|
||||
Key: key,
|
||||
Value: value,
|
||||
OldValue: prev,
|
||||
},
|
||||
}
|
||||
for listener := range t.listeners {
|
||||
t.sendMessageToListener(listener, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) notifyDeleted(key string, prev *TransientDataEntry) {
|
||||
msg := &ServerMessage{
|
||||
Type: "transient",
|
||||
TransientData: &TransientDataServerMessage{
|
||||
Type: "remove",
|
||||
Key: key,
|
||||
},
|
||||
}
|
||||
if prev != nil {
|
||||
msg.TransientData.OldValue = prev.Value
|
||||
}
|
||||
for listener := range t.listeners {
|
||||
t.sendMessageToListener(listener, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// AddListener adds a new listener to be notified about changes.
|
||||
func (t *TransientData) AddListener(listener TransientListener) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.listeners == nil {
|
||||
t.listeners = make(map[TransientListener]bool)
|
||||
}
|
||||
t.listeners[listener] = true
|
||||
if len(t.data) > 0 {
|
||||
data := make(StringMap, len(t.data))
|
||||
for k, v := range t.data {
|
||||
data[k] = v.Value
|
||||
}
|
||||
msg := &ServerMessage{
|
||||
Type: "transient",
|
||||
TransientData: &TransientDataServerMessage{
|
||||
Type: "initial",
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
t.sendMessageToListener(listener, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveListener removes a previously registered listener.
|
||||
func (t *TransientData) RemoveListener(listener TransientListener) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
delete(t.listeners, listener)
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) updateTTL(key string, value any, ttl time.Duration) {
|
||||
if ttl <= 0 {
|
||||
if old, found := t.timers[key]; found {
|
||||
old.Stop()
|
||||
delete(t.timers, key)
|
||||
}
|
||||
} else {
|
||||
t.removeAfterTTL(key, value, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) removeAfterTTL(key string, value any, ttl time.Duration) {
|
||||
if old, found := t.timers[key]; found {
|
||||
old.Stop()
|
||||
}
|
||||
|
||||
if ttl <= 0 {
|
||||
delete(t.timers, key)
|
||||
return
|
||||
}
|
||||
|
||||
timer := time.AfterFunc(ttl, func() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.compareAndRemove(key, value)
|
||||
})
|
||||
if t.timers == nil {
|
||||
t.timers = make(map[string]*time.Timer)
|
||||
}
|
||||
t.timers[key] = timer
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) doSet(key string, value any, prev *TransientDataEntry, ttl time.Duration) {
|
||||
if t.data == nil {
|
||||
t.data = make(TransientDataEntries)
|
||||
}
|
||||
var oldValue any
|
||||
if prev == nil {
|
||||
entry := NewTransientDataEntry(value, ttl)
|
||||
t.data[key] = entry
|
||||
} else {
|
||||
oldValue = prev.Value
|
||||
prev.update(value, ttl)
|
||||
}
|
||||
t.notifySet(key, oldValue, value)
|
||||
t.removeAfterTTL(key, value, ttl)
|
||||
}
|
||||
|
||||
// Set sets a new value for the given key and notifies listeners
|
||||
// if the value has been changed.
|
||||
func (t *TransientData) Set(key string, value any) bool {
|
||||
return t.SetTTL(key, value, 0)
|
||||
}
|
||||
|
||||
// SetTTL sets a new value for the given key with a time-to-live and notifies
|
||||
// listeners if the value has been changed.
|
||||
func (t *TransientData) SetTTL(key string, value any, ttl time.Duration) bool {
|
||||
if value == nil {
|
||||
return t.Remove(key)
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
prev, found := t.data[key]
|
||||
if found && reflect.DeepEqual(prev.Value, value) {
|
||||
t.updateTTL(key, value, ttl)
|
||||
return false
|
||||
}
|
||||
|
||||
t.doSet(key, value, prev, ttl)
|
||||
return true
|
||||
}
|
||||
|
||||
// CompareAndSet sets a new value for the given key only for a given old value
|
||||
// and notifies listeners if the value has been changed.
|
||||
func (t *TransientData) CompareAndSet(key string, old, value any) bool {
|
||||
return t.CompareAndSetTTL(key, old, value, 0)
|
||||
}
|
||||
|
||||
// CompareAndSetTTL sets a new value for the given key with a time-to-live,
|
||||
// only for a given old value and notifies listeners if the value has been
|
||||
// changed.
|
||||
func (t *TransientData) CompareAndSetTTL(key string, old, value any, ttl time.Duration) bool {
|
||||
if value == nil {
|
||||
return t.CompareAndRemove(key, old)
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
prev, found := t.data[key]
|
||||
if old != nil && (!found || !reflect.DeepEqual(prev.Value, old)) {
|
||||
return false
|
||||
} else if old == nil && found {
|
||||
return false
|
||||
}
|
||||
|
||||
t.doSet(key, value, prev, ttl)
|
||||
return true
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) doRemove(key string, prev *TransientDataEntry) {
|
||||
delete(t.data, key)
|
||||
if old, found := t.timers[key]; found {
|
||||
old.Stop()
|
||||
delete(t.timers, key)
|
||||
}
|
||||
t.notifyDeleted(key, prev)
|
||||
}
|
||||
|
||||
// Remove deletes the value with the given key and notifies listeners
|
||||
// if the key was removed.
|
||||
func (t *TransientData) Remove(key string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
prev, found := t.data[key]
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
||||
t.doRemove(key, prev)
|
||||
return true
|
||||
}
|
||||
|
||||
// CompareAndRemove deletes the value with the given key if it has a given value
|
||||
// and notifies listeners if the key was removed.
|
||||
func (t *TransientData) CompareAndRemove(key string, old any) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
return t.compareAndRemove(key, old)
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
func (t *TransientData) compareAndRemove(key string, old any) bool {
|
||||
prev, found := t.data[key]
|
||||
if !found || !reflect.DeepEqual(prev.Value, old) {
|
||||
return false
|
||||
}
|
||||
|
||||
t.doRemove(key, prev)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetData returns a copy of the internal data.
|
||||
func (t *TransientData) GetData() StringMap {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if len(t.data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(StringMap, len(t.data))
|
||||
for k, entry := range t.data {
|
||||
result[k] = entry.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetEntries returns a copy of the internal data entries.
|
||||
func (t *TransientData) GetEntries() TransientDataEntries {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if len(t.data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(TransientDataEntries, len(t.data))
|
||||
for k, e := range t.data {
|
||||
result[k] = e.clone()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SetInitial sets the initial data and notifies listeners.
|
||||
func (t *TransientData) SetInitial(data TransientDataEntries) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.data == nil {
|
||||
t.data = make(TransientDataEntries)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
msgData := make(StringMap, len(data))
|
||||
for k, v := range data {
|
||||
if _, found := t.data[k]; found {
|
||||
// Entry already present (i.e. was set by regular event).
|
||||
continue
|
||||
}
|
||||
|
||||
if e := v.Expires; !e.IsZero() {
|
||||
if now.After(e) {
|
||||
// Already expired
|
||||
continue
|
||||
}
|
||||
|
||||
t.removeAfterTTL(k, v.Value, e.Sub(now))
|
||||
}
|
||||
msgData[k] = v.Value
|
||||
t.data[k] = v
|
||||
}
|
||||
if len(msgData) == 0 {
|
||||
return
|
||||
}
|
||||
msg := &ServerMessage{
|
||||
Type: "transient",
|
||||
TransientData: &TransientDataServerMessage{
|
||||
Type: "initial",
|
||||
Data: msgData,
|
||||
},
|
||||
}
|
||||
for listener := range t.listeners {
|
||||
t.sendMessageToListener(listener, msg)
|
||||
}
|
||||
}
|
||||
234
api/transient_data_test.go
Normal file
234
api/transient_data_test.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2021 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_TransientData(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
data := NewTransientData()
|
||||
assert.False(data.Set("foo", nil))
|
||||
assert.True(data.Set("foo", "bar"))
|
||||
assert.False(data.Set("foo", "bar"))
|
||||
assert.True(data.Set("foo", "baz"))
|
||||
assert.False(data.CompareAndSet("foo", "bar", "lala"))
|
||||
assert.True(data.CompareAndSet("foo", "baz", "lala"))
|
||||
assert.False(data.CompareAndSet("test", nil, nil))
|
||||
assert.True(data.CompareAndSet("test", nil, "123"))
|
||||
assert.False(data.CompareAndSet("test", nil, "456"))
|
||||
assert.False(data.CompareAndRemove("test", "1234"))
|
||||
assert.True(data.CompareAndRemove("test", "123"))
|
||||
assert.False(data.Remove("lala"))
|
||||
assert.True(data.Remove("foo"))
|
||||
|
||||
assert.True(data.SetTTL("test", "1234", time.Millisecond))
|
||||
assert.Equal("1234", data.GetData()["test"])
|
||||
// Data is removed after the TTL
|
||||
start := time.Now()
|
||||
time.Sleep(time.Millisecond)
|
||||
synctest.Wait()
|
||||
assert.Equal(time.Millisecond, time.Since(start))
|
||||
assert.Nil(data.GetData()["test"])
|
||||
|
||||
assert.True(data.SetTTL("test", "1234", time.Millisecond))
|
||||
assert.Equal("1234", data.GetData()["test"])
|
||||
assert.True(data.SetTTL("test", "2345", 3*time.Millisecond))
|
||||
assert.Equal("2345", data.GetData()["test"])
|
||||
start = time.Now()
|
||||
// Data is removed after the TTL only if the value still matches
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
synctest.Wait()
|
||||
assert.Equal("2345", data.GetData()["test"])
|
||||
// Data is removed after the (second) TTL
|
||||
time.Sleep(time.Millisecond)
|
||||
synctest.Wait()
|
||||
assert.Equal(3*time.Millisecond, time.Since(start))
|
||||
assert.Nil(data.GetData()["test"])
|
||||
|
||||
// Setting existing key will update the TTL
|
||||
assert.True(data.SetTTL("test", "1234", time.Millisecond))
|
||||
assert.False(data.SetTTL("test", "1234", 3*time.Millisecond))
|
||||
start = time.Now()
|
||||
// Data still exists after the first TTL
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
synctest.Wait()
|
||||
assert.Equal("1234", data.GetData()["test"])
|
||||
// Data is removed after the (updated) TTL
|
||||
time.Sleep(time.Millisecond)
|
||||
synctest.Wait()
|
||||
assert.Equal(3*time.Millisecond, time.Since(start))
|
||||
assert.Nil(data.GetData()["test"])
|
||||
})
|
||||
}
|
||||
|
||||
type MockTransientListener struct {
|
||||
mu sync.Mutex
|
||||
sending chan struct{}
|
||||
done chan struct{}
|
||||
|
||||
// +checklocks:mu
|
||||
data *TransientData
|
||||
}
|
||||
|
||||
func (l *MockTransientListener) SendMessage(message *ServerMessage) bool {
|
||||
close(l.sending)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
defer close(l.done)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *MockTransientListener) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.data.RemoveListener(l)
|
||||
}
|
||||
|
||||
func Test_TransientDataDeadlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
data := NewTransientData()
|
||||
|
||||
listener := &MockTransientListener{
|
||||
sending: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
|
||||
data: data,
|
||||
}
|
||||
data.AddListener(listener)
|
||||
|
||||
go func() {
|
||||
<-listener.sending
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
data.Set("foo", "bar")
|
||||
<-listener.done
|
||||
}
|
||||
|
||||
type initialDataListener struct {
|
||||
t *testing.T
|
||||
|
||||
expected StringMap
|
||||
sent atomic.Int32
|
||||
}
|
||||
|
||||
func (l *initialDataListener) SendMessage(message *ServerMessage) bool {
|
||||
switch l.sent.Add(1) {
|
||||
case 1:
|
||||
if assert.Equal(l.t, "transient", message.Type) &&
|
||||
assert.NotNil(l.t, message.TransientData) &&
|
||||
assert.Equal(l.t, "initial", message.TransientData.Type) {
|
||||
assert.Equal(l.t, l.expected, message.TransientData.Data)
|
||||
}
|
||||
case 2:
|
||||
if assert.Equal(l.t, "transient", message.Type) &&
|
||||
assert.NotNil(l.t, message.TransientData) &&
|
||||
assert.Equal(l.t, "remove", message.TransientData.Type) {
|
||||
assert.Equal(l.t, "foo", message.TransientData.Key)
|
||||
assert.Equal(l.t, "bar", message.TransientData.OldValue)
|
||||
}
|
||||
default:
|
||||
assert.Fail(l.t, "unexpected message", "received %+v", message)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Test_TransientDataNotifyInitial(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
data := NewTransientData()
|
||||
assert.True(data.Set("foo", "bar"))
|
||||
|
||||
listener := &initialDataListener{
|
||||
t: t,
|
||||
expected: StringMap{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
data.AddListener(listener)
|
||||
assert.EqualValues(1, listener.sent.Load())
|
||||
}
|
||||
|
||||
func Test_TransientDataSetInitial(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
now := time.Now()
|
||||
data := NewTransientData()
|
||||
listener1 := &initialDataListener{
|
||||
t: t,
|
||||
expected: StringMap{
|
||||
"foo": "bar",
|
||||
"bar": 1234,
|
||||
},
|
||||
}
|
||||
data.AddListener(listener1)
|
||||
assert.EqualValues(0, listener1.sent.Load())
|
||||
|
||||
data.SetInitial(TransientDataEntries{
|
||||
"foo": NewTransientDataEntryWithExpires("bar", now.Add(time.Minute)),
|
||||
"bar": NewTransientDataEntry(1234, 0),
|
||||
"expired": NewTransientDataEntryWithExpires(1234, now.Add(-time.Second)),
|
||||
})
|
||||
|
||||
entries := data.GetEntries()
|
||||
assert.Equal(TransientDataEntries{
|
||||
"foo": NewTransientDataEntryWithExpires("bar", now.Add(time.Minute)),
|
||||
"bar": NewTransientDataEntry(1234, 0),
|
||||
}, entries)
|
||||
|
||||
listener2 := &initialDataListener{
|
||||
t: t,
|
||||
expected: StringMap{
|
||||
"foo": "bar",
|
||||
"bar": 1234,
|
||||
},
|
||||
}
|
||||
data.AddListener(listener2)
|
||||
assert.EqualValues(1, listener1.sent.Load())
|
||||
assert.EqualValues(1, listener2.sent.Load())
|
||||
|
||||
time.Sleep(time.Minute)
|
||||
synctest.Wait()
|
||||
assert.EqualValues(2, listener1.sent.Load())
|
||||
assert.EqualValues(2, listener2.sent.Load())
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,426 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2017 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type testCheckValid interface {
|
||||
CheckValid() error
|
||||
}
|
||||
|
||||
func wrapMessage(messageType string, msg testCheckValid) *ClientMessage {
|
||||
wrapped := &ClientMessage{
|
||||
Type: messageType,
|
||||
}
|
||||
switch messageType {
|
||||
case "hello":
|
||||
wrapped.Hello = msg.(*HelloClientMessage)
|
||||
case "message":
|
||||
wrapped.Message = msg.(*MessageClientMessage)
|
||||
case "bye":
|
||||
wrapped.Bye = msg.(*ByeClientMessage)
|
||||
case "room":
|
||||
wrapped.Room = msg.(*RoomClientMessage)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
|
||||
func testMessages(t *testing.T, messageType string, valid_messages []testCheckValid, invalid_messages []testCheckValid) {
|
||||
t.Helper()
|
||||
assert := assert.New(t)
|
||||
for _, msg := range valid_messages {
|
||||
assert.NoError(msg.CheckValid(), "Message %+v should be valid", msg)
|
||||
|
||||
// If the inner message is valid, it should also be valid in a wrapped
|
||||
// ClientMessage.
|
||||
if wrapped := wrapMessage(messageType, msg); assert.NotNil(wrapped, "Unknown message type: %s", messageType) {
|
||||
assert.NoError(wrapped.CheckValid(), "Message %+v should be valid", wrapped)
|
||||
}
|
||||
}
|
||||
for _, msg := range invalid_messages {
|
||||
assert.Error(msg.CheckValid(), "Message %+v should not be valid", msg)
|
||||
|
||||
// If the inner message is invalid, it should also be invalid in a
|
||||
// wrapped ClientMessage.
|
||||
if wrapped := wrapMessage(messageType, msg); assert.NotNil(wrapped, "Unknown message type: %s", messageType) {
|
||||
assert.Error(wrapped.CheckValid(), "Message %+v should not be valid", wrapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
// The message needs a type.
|
||||
msg := ClientMessage{}
|
||||
assert.Error(msg.CheckValid())
|
||||
}
|
||||
|
||||
func TestHelloClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
internalAuthParams := []byte("{\"backend\":\"https://domain.invalid\"}")
|
||||
tokenAuthParams := []byte("{\"token\":\"invalid-token\"}")
|
||||
valid_messages := []testCheckValid{
|
||||
// Hello version 1
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "client",
|
||||
Params: json.RawMessage("{}"),
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "internal",
|
||||
Params: internalAuthParams,
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
ResumeId: "the-resume-id",
|
||||
},
|
||||
// Hello version 2
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: tokenAuthParams,
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "client",
|
||||
Params: tokenAuthParams,
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
ResumeId: "the-resume-id",
|
||||
},
|
||||
}
|
||||
invalid_messages := []testCheckValid{
|
||||
// Hello version 1
|
||||
&HelloClientMessage{},
|
||||
&HelloClientMessage{Version: "0.0"},
|
||||
&HelloClientMessage{Version: HelloVersionV1},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
Type: "invalid-type",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
Url: "invalid-url",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "internal",
|
||||
Params: json.RawMessage("{}"),
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "internal",
|
||||
Params: json.RawMessage("xyz"), // Invalid JSON.
|
||||
},
|
||||
},
|
||||
// Hello version 2
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: tokenAuthParams,
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: tokenAuthParams,
|
||||
Url: "invalid-url",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: internalAuthParams,
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersionV2,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("xyz"), // Invalid JSON.
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testMessages(t, "hello", valid_messages, invalid_messages)
|
||||
|
||||
// A "hello" message must be present
|
||||
msg := ClientMessage{
|
||||
Type: "hello",
|
||||
}
|
||||
assert := assert.New(t)
|
||||
assert.Error(msg.CheckValid())
|
||||
}
|
||||
|
||||
func TestMessageClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid_messages := []testCheckValid{
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
SessionId: "the-session-id",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "user",
|
||||
UserId: "the-user-id",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "room",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
}
|
||||
invalid_messages := []testCheckValid{
|
||||
&MessageClientMessage{},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
SessionId: "the-session-id",
|
||||
},
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
UserId: "the-user-id",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "user",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "user",
|
||||
UserId: "the-user-id",
|
||||
},
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "user",
|
||||
SessionId: "the-user-id",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "unknown-type",
|
||||
},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
}
|
||||
testMessages(t, "message", valid_messages, invalid_messages)
|
||||
|
||||
// A "message" message must be present
|
||||
msg := ClientMessage{
|
||||
Type: "message",
|
||||
}
|
||||
assert := assert.New(t)
|
||||
assert.Error(msg.CheckValid())
|
||||
}
|
||||
|
||||
func TestByeClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Any "bye" message is valid.
|
||||
valid_messages := []testCheckValid{
|
||||
&ByeClientMessage{},
|
||||
}
|
||||
invalid_messages := []testCheckValid{}
|
||||
|
||||
testMessages(t, "bye", valid_messages, invalid_messages)
|
||||
|
||||
// The "bye" message is optional.
|
||||
msg := ClientMessage{
|
||||
Type: "bye",
|
||||
}
|
||||
assert := assert.New(t)
|
||||
assert.NoError(msg.CheckValid())
|
||||
}
|
||||
|
||||
func TestRoomClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Any "room" message is valid.
|
||||
valid_messages := []testCheckValid{
|
||||
&RoomClientMessage{},
|
||||
}
|
||||
invalid_messages := []testCheckValid{}
|
||||
|
||||
testMessages(t, "room", valid_messages, invalid_messages)
|
||||
|
||||
// But a "room" message must be present
|
||||
msg := ClientMessage{
|
||||
Type: "room",
|
||||
}
|
||||
assert := assert.New(t)
|
||||
assert.Error(msg.CheckValid())
|
||||
}
|
||||
|
||||
func TestErrorMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
id := "request-id"
|
||||
msg := ClientMessage{
|
||||
Id: id,
|
||||
}
|
||||
err1 := msg.NewErrorServerMessage(&Error{})
|
||||
assert.Equal(id, err1.Id, "%+v", err1)
|
||||
assert.Equal("error", err1.Type, "%+v", err1)
|
||||
assert.NotNil(err1.Error, "%+v", err1)
|
||||
|
||||
err2 := msg.NewWrappedErrorServerMessage(fmt.Errorf("test-error"))
|
||||
assert.Equal(id, err2.Id, "%+v", err2)
|
||||
assert.Equal("error", err2.Type, "%+v", err2)
|
||||
if assert.NotNil(err2.Error, "%+v", err2) {
|
||||
assert.Equal("internal_error", err2.Error.Code, "%+v", err2)
|
||||
assert.Equal("test-error", err2.Error.Message, "%+v", err2)
|
||||
}
|
||||
// Test "error" interface
|
||||
assert.Equal("test-error", err2.Error.Error(), "%+v", err2)
|
||||
}
|
||||
|
||||
func TestIsChatRefresh(t *testing.T) {
|
||||
t.Parallel()
|
||||
var msg ServerMessage
|
||||
data_true := []byte("{\"type\":\"chat\",\"chat\":{\"refresh\":true}}")
|
||||
msg = ServerMessage{
|
||||
Type: "message",
|
||||
Message: &MessageServerMessage{
|
||||
Data: data_true,
|
||||
},
|
||||
}
|
||||
assert.True(t, msg.IsChatRefresh())
|
||||
|
||||
data_false := []byte("{\"type\":\"chat\",\"chat\":{\"refresh\":false}}")
|
||||
msg = ServerMessage{
|
||||
Type: "message",
|
||||
Message: &MessageServerMessage{
|
||||
Data: data_false,
|
||||
},
|
||||
}
|
||||
assert.False(t, msg.IsChatRefresh())
|
||||
}
|
||||
|
||||
func assertEqualStrings(t *testing.T, expected, result []string) {
|
||||
t.Helper()
|
||||
|
||||
if expected == nil {
|
||||
expected = make([]string, 0)
|
||||
} else {
|
||||
sort.Strings(expected)
|
||||
}
|
||||
if result == nil {
|
||||
result = make([]string, 0)
|
||||
} else {
|
||||
sort.Strings(result)
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func Test_Welcome_AddRemoveFeature(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
var msg WelcomeServerMessage
|
||||
assertEqualStrings(t, []string{}, msg.Features)
|
||||
|
||||
msg.AddFeature("one", "two", "one")
|
||||
assertEqualStrings(t, []string{"one", "two"}, msg.Features)
|
||||
assert.True(sort.StringsAreSorted(msg.Features), "features should be sorted, got %+v", msg.Features)
|
||||
|
||||
msg.AddFeature("three")
|
||||
assertEqualStrings(t, []string{"one", "two", "three"}, msg.Features)
|
||||
assert.True(sort.StringsAreSorted(msg.Features), "features should be sorted, got %+v", msg.Features)
|
||||
|
||||
msg.RemoveFeature("three", "one")
|
||||
assertEqualStrings(t, []string{"two"}, msg.Features)
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -41,10 +41,10 @@ type exponentialBackoff struct {
|
|||
|
||||
func NewExponentialBackoff(initial time.Duration, maxWait time.Duration) (Backoff, error) {
|
||||
if initial <= 0 {
|
||||
return nil, fmt.Errorf("initial must be larger than 0")
|
||||
return nil, errors.New("initial must be larger than 0")
|
||||
}
|
||||
if maxWait < initial {
|
||||
return nil, fmt.Errorf("maxWait must be larger or equal to initial")
|
||||
return nil, errors.New("maxWait must be larger or equal to initial")
|
||||
}
|
||||
|
||||
return &exponentialBackoff{
|
||||
|
|
@ -67,10 +67,6 @@ func (b *exponentialBackoff) Wait(ctx context.Context) {
|
|||
waiter, cancel := context.WithTimeout(ctx, b.nextWait)
|
||||
defer cancel()
|
||||
|
||||
b.nextWait = b.nextWait * 2
|
||||
if b.nextWait > b.maxWait {
|
||||
b.nextWait = b.maxWait
|
||||
}
|
||||
|
||||
b.nextWait = min(b.nextWait*2, b.maxWait)
|
||||
<-waiter.Done()
|
||||
}
|
||||
|
|
@ -19,11 +19,12 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -32,31 +33,32 @@ import (
|
|||
|
||||
func TestBackoff_Exponential(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
minWait := 100 * time.Millisecond
|
||||
backoff, err := NewExponentialBackoff(minWait, 500*time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
minWait := 100 * time.Millisecond
|
||||
backoff, err := NewExponentialBackoff(minWait, 500*time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
waitTimes := []time.Duration{
|
||||
minWait,
|
||||
200 * time.Millisecond,
|
||||
400 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
}
|
||||
waitTimes := []time.Duration{
|
||||
minWait,
|
||||
200 * time.Millisecond,
|
||||
400 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
}
|
||||
|
||||
for _, wait := range waitTimes {
|
||||
assert.Equal(wait, backoff.NextWait())
|
||||
for _, wait := range waitTimes {
|
||||
assert.Equal(wait, backoff.NextWait())
|
||||
a := time.Now()
|
||||
backoff.Wait(context.Background())
|
||||
b := time.Now()
|
||||
assert.Equal(b.Sub(a), wait)
|
||||
}
|
||||
|
||||
backoff.Reset()
|
||||
a := time.Now()
|
||||
backoff.Wait(context.Background())
|
||||
b := time.Now()
|
||||
assert.GreaterOrEqual(b.Sub(a), wait)
|
||||
}
|
||||
|
||||
backoff.Reset()
|
||||
a := time.Now()
|
||||
backoff.Wait(context.Background())
|
||||
b := time.Now()
|
||||
assert.GreaterOrEqual(b.Sub(a), minWait)
|
||||
assert.Equal(b.Sub(a), minWait)
|
||||
})
|
||||
}
|
||||
|
|
@ -19,29 +19,32 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
)
|
||||
|
||||
// DeferredExecutor will asynchronously execute functions while maintaining
|
||||
// their order.
|
||||
type DeferredExecutor struct {
|
||||
logger log.Logger
|
||||
queue chan func()
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewDeferredExecutor(queueSize int) *DeferredExecutor {
|
||||
func NewDeferredExecutor(logger log.Logger, queueSize int) *DeferredExecutor {
|
||||
if queueSize < 0 {
|
||||
queueSize = 0
|
||||
}
|
||||
result := &DeferredExecutor{
|
||||
logger: logger,
|
||||
queue: make(chan func(), queueSize),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
|
@ -62,15 +65,15 @@ func (e *DeferredExecutor) run() {
|
|||
}
|
||||
}
|
||||
|
||||
func getFunctionName(i interface{}) string {
|
||||
func getFunctionName(i any) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
func (e *DeferredExecutor) Execute(f func()) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Printf("Could not defer function %v: %+v", getFunctionName(f), e)
|
||||
log.Printf("Called from %s", string(debug.Stack()))
|
||||
if err := recover(); err != nil {
|
||||
e.logger.Printf("Could not defer function %v: %+v", getFunctionName(f), err)
|
||||
e.logger.Printf("Called from %s", string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -19,17 +19,22 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
)
|
||||
|
||||
func TestDeferredExecutor_MultiClose(t *testing.T) {
|
||||
e := NewDeferredExecutor(0)
|
||||
t.Parallel()
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
e := NewDeferredExecutor(logger, 0)
|
||||
defer e.waitForStop()
|
||||
|
||||
e.Close()
|
||||
|
|
@ -38,28 +43,32 @@ func TestDeferredExecutor_MultiClose(t *testing.T) {
|
|||
|
||||
func TestDeferredExecutor_QueueSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := NewDeferredExecutor(0)
|
||||
defer e.waitForStop()
|
||||
defer e.Close()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
e := NewDeferredExecutor(logger, 0)
|
||||
defer e.waitForStop()
|
||||
defer e.Close()
|
||||
|
||||
delay := 100 * time.Millisecond
|
||||
e.Execute(func() {
|
||||
time.Sleep(delay)
|
||||
})
|
||||
delay := 100 * time.Millisecond
|
||||
e.Execute(func() {
|
||||
time.Sleep(delay)
|
||||
})
|
||||
|
||||
// The queue will block until the first command finishes.
|
||||
a := time.Now()
|
||||
e.Execute(func() {
|
||||
time.Sleep(time.Millisecond)
|
||||
// The queue will block until the first command finishes.
|
||||
a := time.Now()
|
||||
e.Execute(func() {
|
||||
time.Sleep(time.Millisecond)
|
||||
})
|
||||
b := time.Now()
|
||||
delta := b.Sub(a)
|
||||
assert.Equal(t, delay, delta)
|
||||
})
|
||||
b := time.Now()
|
||||
delta := b.Sub(a)
|
||||
// Allow one millisecond less delay to account for time variance on CI runners.
|
||||
assert.GreaterOrEqual(t, delta+time.Millisecond, delay)
|
||||
}
|
||||
|
||||
func TestDeferredExecutor_Order(t *testing.T) {
|
||||
e := NewDeferredExecutor(64)
|
||||
t.Parallel()
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
e := NewDeferredExecutor(logger, 64)
|
||||
defer e.waitForStop()
|
||||
defer e.Close()
|
||||
|
||||
|
|
@ -71,7 +80,7 @@ func TestDeferredExecutor_Order(t *testing.T) {
|
|||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
for x := 0; x < 10; x++ {
|
||||
for x := range 10 {
|
||||
e.Execute(getFunc(x))
|
||||
}
|
||||
|
||||
|
|
@ -80,13 +89,15 @@ func TestDeferredExecutor_Order(t *testing.T) {
|
|||
})
|
||||
<-done
|
||||
|
||||
for x := 0; x < 10; x++ {
|
||||
for x := range 10 {
|
||||
assert.Equal(t, entries[x], x, "Unexpected at position %d", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeferredExecutor_CloseFromFunc(t *testing.T) {
|
||||
e := NewDeferredExecutor(64)
|
||||
t.Parallel()
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
e := NewDeferredExecutor(logger, 64)
|
||||
defer e.waitForStop()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
|
@ -99,8 +110,9 @@ func TestDeferredExecutor_CloseFromFunc(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeferredExecutor_DeferAfterClose(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
e := NewDeferredExecutor(64)
|
||||
t.Parallel()
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
e := NewDeferredExecutor(logger, 64)
|
||||
defer e.waitForStop()
|
||||
|
||||
e.Close()
|
||||
|
|
@ -111,7 +123,9 @@ func TestDeferredExecutor_DeferAfterClose(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeferredExecutor_WaitForStopTwice(t *testing.T) {
|
||||
e := NewDeferredExecutor(64)
|
||||
t.Parallel()
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
e := NewDeferredExecutor(logger, 64)
|
||||
defer e.waitForStop()
|
||||
|
||||
e.Close()
|
||||
|
|
@ -19,12 +19,15 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
type AsyncMessage struct {
|
||||
|
|
@ -32,11 +35,11 @@ type AsyncMessage struct {
|
|||
|
||||
Type string `json:"type"`
|
||||
|
||||
Message *ServerMessage `json:"message,omitempty"`
|
||||
Message *api.ServerMessage `json:"message,omitempty"`
|
||||
|
||||
Room *BackendServerRoomRequest `json:"room,omitempty"`
|
||||
Room *talk.BackendServerRoomRequest `json:"room,omitempty"`
|
||||
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
Permissions []api.Permission `json:"permissions,omitempty"`
|
||||
|
||||
AsyncRoom *AsyncRoomMessage `json:"asyncroom,omitempty"`
|
||||
|
||||
|
|
@ -56,12 +59,12 @@ func (m *AsyncMessage) String() string {
|
|||
type AsyncRoomMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
SessionId string `json:"sessionid,omitempty"`
|
||||
ClientType string `json:"clienttype,omitempty"`
|
||||
SessionId api.PublicSessionId `json:"sessionid,omitempty"`
|
||||
ClientType api.ClientType `json:"clienttype,omitempty"`
|
||||
}
|
||||
|
||||
type SendOfferMessage struct {
|
||||
MessageId string `json:"messageid,omitempty"`
|
||||
SessionId string `json:"sessionid"`
|
||||
Data *MessageClientMessageData `json:"data"`
|
||||
MessageId string `json:"messageid,omitempty"`
|
||||
SessionId api.PublicSessionId `json:"sessionid"`
|
||||
Data *api.MessageClientMessageData `json:"data"`
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
||||
|
||||
package signaling
|
||||
package events
|
||||
|
||||
import (
|
||||
json "encoding/json"
|
||||
easyjson "github.com/mailru/easyjson"
|
||||
jlexer "github.com/mailru/easyjson/jlexer"
|
||||
jwriter "github.com/mailru/easyjson/jwriter"
|
||||
api "github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
talk "github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
// suppress unused package warning
|
||||
|
|
@ -17,7 +19,7 @@ var (
|
|||
_ easyjson.Marshaler
|
||||
)
|
||||
|
||||
func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexer.Lexer, out *SendOfferMessage) {
|
||||
func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(in *jlexer.Lexer, out *SendOfferMessage) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
|
|
@ -30,25 +32,32 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexe
|
|||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "messageid":
|
||||
out.MessageId = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.MessageId = string(in.String())
|
||||
}
|
||||
case "sessionid":
|
||||
out.SessionId = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.SessionId = api.PublicSessionId(in.String())
|
||||
}
|
||||
case "data":
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
out.Data = nil
|
||||
} else {
|
||||
if out.Data == nil {
|
||||
out.Data = new(MessageClientMessageData)
|
||||
out.Data = new(api.MessageClientMessageData)
|
||||
}
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
(*out.Data).UnmarshalEasyJSON(in)
|
||||
}
|
||||
(*out.Data).UnmarshalEasyJSON(in)
|
||||
}
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
|
|
@ -60,7 +69,7 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexe
|
|||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwriter.Writer, in SendOfferMessage) {
|
||||
func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(out *jwriter.Writer, in SendOfferMessage) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
|
|
@ -95,27 +104,27 @@ func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwri
|
|||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v SendOfferMessage) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(&w, v)
|
||||
easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v SendOfferMessage) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(w, v)
|
||||
easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *SendOfferMessage) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(&r, v)
|
||||
easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *SendOfferMessage) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(l, v)
|
||||
easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(l, v)
|
||||
}
|
||||
func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlexer.Lexer, out *AsyncRoomMessage) {
|
||||
func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(in *jlexer.Lexer, out *AsyncRoomMessage) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
|
|
@ -128,18 +137,25 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlex
|
|||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "type":
|
||||
out.Type = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.Type = string(in.String())
|
||||
}
|
||||
case "sessionid":
|
||||
out.SessionId = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.SessionId = api.PublicSessionId(in.String())
|
||||
}
|
||||
case "clienttype":
|
||||
out.ClientType = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.ClientType = api.ClientType(in.String())
|
||||
}
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
|
|
@ -150,7 +166,7 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlex
|
|||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(out *jwriter.Writer, in AsyncRoomMessage) {
|
||||
func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(out *jwriter.Writer, in AsyncRoomMessage) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
|
|
@ -175,27 +191,27 @@ func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(out *jwr
|
|||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v AsyncRoomMessage) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(&w, v)
|
||||
easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v AsyncRoomMessage) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(w, v)
|
||||
easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *AsyncRoomMessage) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(&r, v)
|
||||
easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *AsyncRoomMessage) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(l, v)
|
||||
easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(l, v)
|
||||
}
|
||||
func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlexer.Lexer, out *AsyncMessage) {
|
||||
func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(in *jlexer.Lexer, out *AsyncMessage) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
|
|
@ -208,27 +224,34 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlex
|
|||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "sendtime":
|
||||
if data := in.Raw(); in.Ok() {
|
||||
in.AddError((out.SendTime).UnmarshalJSON(data))
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
if data := in.Raw(); in.Ok() {
|
||||
in.AddError((out.SendTime).UnmarshalJSON(data))
|
||||
}
|
||||
}
|
||||
case "type":
|
||||
out.Type = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.Type = string(in.String())
|
||||
}
|
||||
case "message":
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
out.Message = nil
|
||||
} else {
|
||||
if out.Message == nil {
|
||||
out.Message = new(ServerMessage)
|
||||
out.Message = new(api.ServerMessage)
|
||||
}
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
(*out.Message).UnmarshalEasyJSON(in)
|
||||
}
|
||||
(*out.Message).UnmarshalEasyJSON(in)
|
||||
}
|
||||
case "room":
|
||||
if in.IsNull() {
|
||||
|
|
@ -236,9 +259,13 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlex
|
|||
out.Room = nil
|
||||
} else {
|
||||
if out.Room == nil {
|
||||
out.Room = new(BackendServerRoomRequest)
|
||||
out.Room = new(talk.BackendServerRoomRequest)
|
||||
}
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
(*out.Room).UnmarshalEasyJSON(in)
|
||||
}
|
||||
(*out.Room).UnmarshalEasyJSON(in)
|
||||
}
|
||||
case "permissions":
|
||||
if in.IsNull() {
|
||||
|
|
@ -248,16 +275,20 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlex
|
|||
in.Delim('[')
|
||||
if out.Permissions == nil {
|
||||
if !in.IsDelim(']') {
|
||||
out.Permissions = make([]Permission, 0, 4)
|
||||
out.Permissions = make([]api.Permission, 0, 4)
|
||||
} else {
|
||||
out.Permissions = []Permission{}
|
||||
out.Permissions = []api.Permission{}
|
||||
}
|
||||
} else {
|
||||
out.Permissions = (out.Permissions)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v1 Permission
|
||||
v1 = Permission(in.String())
|
||||
var v1 api.Permission
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
v1 = api.Permission(in.String())
|
||||
}
|
||||
out.Permissions = append(out.Permissions, v1)
|
||||
in.WantComma()
|
||||
}
|
||||
|
|
@ -271,7 +302,11 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlex
|
|||
if out.AsyncRoom == nil {
|
||||
out.AsyncRoom = new(AsyncRoomMessage)
|
||||
}
|
||||
(*out.AsyncRoom).UnmarshalEasyJSON(in)
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
(*out.AsyncRoom).UnmarshalEasyJSON(in)
|
||||
}
|
||||
}
|
||||
case "sendoffer":
|
||||
if in.IsNull() {
|
||||
|
|
@ -281,10 +316,18 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlex
|
|||
if out.SendOffer == nil {
|
||||
out.SendOffer = new(SendOfferMessage)
|
||||
}
|
||||
(*out.SendOffer).UnmarshalEasyJSON(in)
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
(*out.SendOffer).UnmarshalEasyJSON(in)
|
||||
}
|
||||
}
|
||||
case "id":
|
||||
out.Id = string(in.String())
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
} else {
|
||||
out.Id = string(in.String())
|
||||
}
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
|
|
@ -295,7 +338,7 @@ func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlex
|
|||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(out *jwriter.Writer, in AsyncMessage) {
|
||||
func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(out *jwriter.Writer, in AsyncMessage) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
|
|
@ -354,23 +397,23 @@ func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(out *jwr
|
|||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v AsyncMessage) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(&w, v)
|
||||
easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v AsyncMessage) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(w, v)
|
||||
easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *AsyncMessage) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(&r, v)
|
||||
easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *AsyncMessage) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(l, v)
|
||||
easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(l, v)
|
||||
}
|
||||
76
async/events/async_events.go
Normal file
76
async/events/async_events.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/nats"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlreadyRegistered = errors.New("already registered") // +checklocksignore: Global readonly variable.
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAsyncChannelSize = 64
|
||||
)
|
||||
|
||||
type AsyncChannel chan *nats.Msg
|
||||
|
||||
type AsyncEventListener interface {
|
||||
AsyncChannel() AsyncChannel
|
||||
}
|
||||
|
||||
type AsyncEvents interface {
|
||||
Close(ctx context.Context) error
|
||||
|
||||
RegisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error
|
||||
UnregisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error
|
||||
|
||||
RegisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error
|
||||
UnregisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error
|
||||
|
||||
RegisterUserListener(userId string, backend *talk.Backend, listener AsyncEventListener) error
|
||||
UnregisterUserListener(userId string, backend *talk.Backend, listener AsyncEventListener) error
|
||||
|
||||
RegisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error
|
||||
UnregisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error
|
||||
|
||||
PublishBackendRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error
|
||||
PublishRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error
|
||||
PublishUserMessage(userId string, backend *talk.Backend, message *AsyncMessage) error
|
||||
PublishSessionMessage(sessionId api.PublicSessionId, backend *talk.Backend, message *AsyncMessage) error
|
||||
}
|
||||
|
||||
func NewAsyncEvents(ctx context.Context, url string) (AsyncEvents, error) {
|
||||
client, err := nats.NewClient(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewAsyncEventsNats(log.LoggerFromContext(ctx), client)
|
||||
}
|
||||
292
async/events/async_events_nats.go
Normal file
292
async/events/async_events_nats.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/nats"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
func GetSubjectForBackendRoomId(roomId string, backend *talk.Backend) string {
|
||||
if backend == nil || backend.IsCompat() {
|
||||
return nats.GetEncodedSubject("backend.room", roomId)
|
||||
}
|
||||
|
||||
return nats.GetEncodedSubject("backend.room", roomId+"|"+backend.Id())
|
||||
}
|
||||
|
||||
func GetSubjectForRoomId(roomId string, backend *talk.Backend) string {
|
||||
if backend == nil || backend.IsCompat() {
|
||||
return nats.GetEncodedSubject("room", roomId)
|
||||
}
|
||||
|
||||
return nats.GetEncodedSubject("room", roomId+"|"+backend.Id())
|
||||
}
|
||||
|
||||
func GetSubjectForUserId(userId string, backend *talk.Backend) string {
|
||||
if backend == nil || backend.IsCompat() {
|
||||
return nats.GetEncodedSubject("user", userId)
|
||||
}
|
||||
|
||||
return nats.GetEncodedSubject("user", userId+"|"+backend.Id())
|
||||
}
|
||||
|
||||
func GetSubjectForSessionId(sessionId api.PublicSessionId, backend *talk.Backend) string {
|
||||
return string("session." + sessionId)
|
||||
}
|
||||
|
||||
type asyncEventsNatsSubscriptions map[string]map[AsyncEventListener]nats.Subscription
|
||||
|
||||
type asyncEventsNats struct {
|
||||
mu sync.Mutex
|
||||
client nats.Client
|
||||
logger log.Logger // +checklocksignore
|
||||
|
||||
// +checklocks:mu
|
||||
backendRoomSubscriptions asyncEventsNatsSubscriptions
|
||||
// +checklocks:mu
|
||||
roomSubscriptions asyncEventsNatsSubscriptions
|
||||
// +checklocks:mu
|
||||
userSubscriptions asyncEventsNatsSubscriptions
|
||||
// +checklocks:mu
|
||||
sessionSubscriptions asyncEventsNatsSubscriptions
|
||||
}
|
||||
|
||||
func NewAsyncEventsNats(logger log.Logger, client nats.Client) (AsyncEvents, error) {
|
||||
events := &asyncEventsNats{
|
||||
client: client,
|
||||
logger: logger,
|
||||
|
||||
backendRoomSubscriptions: make(asyncEventsNatsSubscriptions),
|
||||
roomSubscriptions: make(asyncEventsNatsSubscriptions),
|
||||
userSubscriptions: make(asyncEventsNatsSubscriptions),
|
||||
sessionSubscriptions: make(asyncEventsNatsSubscriptions),
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) GetNatsClient() nats.Client {
|
||||
return e.client
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) GetServerInfoNats() *talk.BackendServerInfoNats {
|
||||
// TODO: This should call a method on "e.client" directly instead of having a type switch.
|
||||
var result *talk.BackendServerInfoNats
|
||||
switch n := e.client.(type) {
|
||||
case *nats.NativeClient:
|
||||
result = &talk.BackendServerInfoNats{
|
||||
Urls: n.URLs(),
|
||||
}
|
||||
if n.IsConnected() {
|
||||
result.Connected = true
|
||||
result.ServerUrl = n.ConnectedUrl()
|
||||
result.ServerID = n.ConnectedServerId()
|
||||
result.ServerVersion = n.ConnectedServerVersion()
|
||||
result.ClusterName = n.ConnectedClusterName()
|
||||
}
|
||||
case *nats.LoopbackClient:
|
||||
result = &talk.BackendServerInfoNats{
|
||||
Urls: []string{nats.LoopbackUrl},
|
||||
Connected: true,
|
||||
ServerUrl: nats.LoopbackUrl,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func closeSubscriptions(logger log.Logger, wg *sync.WaitGroup, subscriptions asyncEventsNatsSubscriptions) {
|
||||
defer wg.Done()
|
||||
|
||||
for subject, subs := range subscriptions {
|
||||
for _, sub := range subs {
|
||||
if err := sub.Unsubscribe(); err != nil && !errors.Is(err, nats.ErrConnectionClosed) {
|
||||
logger.Printf("Error unsubscribing %s: %s", subject, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) Close(ctx context.Context) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go closeSubscriptions(e.logger, &wg, e.backendRoomSubscriptions)
|
||||
wg.Add(1)
|
||||
go closeSubscriptions(e.logger, &wg, e.roomSubscriptions)
|
||||
wg.Add(1)
|
||||
go closeSubscriptions(e.logger, &wg, e.userSubscriptions)
|
||||
wg.Add(1)
|
||||
go closeSubscriptions(e.logger, &wg, e.sessionSubscriptions)
|
||||
// Can't use clear(...) here as the maps are processed asynchronously by the
|
||||
// goroutines above.
|
||||
e.backendRoomSubscriptions = make(asyncEventsNatsSubscriptions)
|
||||
e.roomSubscriptions = make(asyncEventsNatsSubscriptions)
|
||||
e.userSubscriptions = make(asyncEventsNatsSubscriptions)
|
||||
e.sessionSubscriptions = make(asyncEventsNatsSubscriptions)
|
||||
wg.Wait()
|
||||
return e.client.Close(ctx)
|
||||
}
|
||||
|
||||
// +checklocks:e.mu
|
||||
func (e *asyncEventsNats) registerListener(key string, subscriptions asyncEventsNatsSubscriptions, listener AsyncEventListener) error {
|
||||
subs, found := subscriptions[key]
|
||||
if !found {
|
||||
subs = make(map[AsyncEventListener]nats.Subscription)
|
||||
subscriptions[key] = subs
|
||||
} else if _, found := subs[listener]; found {
|
||||
return ErrAlreadyRegistered
|
||||
}
|
||||
|
||||
sub, err := e.client.Subscribe(key, listener.AsyncChannel())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subs[listener] = sub
|
||||
return nil
|
||||
}
|
||||
|
||||
// +checklocks:e.mu
|
||||
func (e *asyncEventsNats) unregisterListener(key string, subscriptions asyncEventsNatsSubscriptions, listener AsyncEventListener) error {
|
||||
subs, found := subscriptions[key]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
sub, found := subs[listener]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
delete(subs, listener)
|
||||
if len(subs) == 0 {
|
||||
delete(subscriptions, key)
|
||||
}
|
||||
|
||||
return sub.Unsubscribe()
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForBackendRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.registerListener(key, e.backendRoomSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForBackendRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.unregisterListener(key, e.backendRoomSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.registerListener(key, e.roomSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.unregisterListener(key, e.roomSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterUserListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForUserId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.registerListener(key, e.userSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterUserListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForUserId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.unregisterListener(key, e.userSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForSessionId(sessionId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.registerListener(key, e.sessionSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error {
|
||||
key := GetSubjectForSessionId(sessionId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.unregisterListener(key, e.sessionSubscriptions, listener)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) publish(subject string, message *AsyncMessage) error {
|
||||
message.SendTime = time.Now().Truncate(time.Microsecond)
|
||||
return e.client.Publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishBackendRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForBackendRoomId(roomId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForRoomId(roomId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishUserMessage(userId string, backend *talk.Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForUserId(userId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishSessionMessage(sessionId api.PublicSessionId, backend *talk.Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForSessionId(sessionId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
38
async/events/async_events_nats_test.go
Normal file
38
async/events/async_events_nats_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package events
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
func Benchmark_GetSubjectForSessionId(b *testing.B) {
|
||||
backend := talk.NewCompatBackend(nil)
|
||||
sid := api.PublicSessionId(internal.RandomString(256))
|
||||
for b.Loop() {
|
||||
GetSubjectForSessionId(sid, backend)
|
||||
}
|
||||
}
|
||||
170
async/events/async_events_test.go
Normal file
170
async/events/async_events_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/nats"
|
||||
natstest "github.com/strukturag/nextcloud-spreed-signaling/v2/nats/test"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
type TestBackendRoomListener struct {
|
||||
events AsyncChannel
|
||||
}
|
||||
|
||||
func (l *TestBackendRoomListener) AsyncChannel() AsyncChannel {
|
||||
return l.events
|
||||
}
|
||||
|
||||
func testAsyncEvents(t *testing.T, events AsyncEvents) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
assert.NoError(events.Close(ctx))
|
||||
})
|
||||
|
||||
listener := &TestBackendRoomListener{
|
||||
events: make(AsyncChannel, 1),
|
||||
}
|
||||
|
||||
roomId := "1234"
|
||||
backend := talk.NewCompatBackend(nil)
|
||||
require.NoError(events.RegisterBackendRoomListener(roomId, backend, listener))
|
||||
defer func() {
|
||||
assert.NoError(events.UnregisterBackendRoomListener(roomId, backend, listener))
|
||||
}()
|
||||
|
||||
msg := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: &talk.BackendServerRoomRequest{
|
||||
Type: "test",
|
||||
},
|
||||
}
|
||||
if assert.NoError(events.PublishBackendRoomMessage(roomId, backend, msg)) {
|
||||
received := <-listener.events
|
||||
var receivedMsg AsyncMessage
|
||||
if assert.NoError(nats.Decode(received, &receivedMsg)) {
|
||||
assert.True(msg.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", msg.SendTime, receivedMsg.SendTime)
|
||||
receivedMsg.SendTime = msg.SendTime
|
||||
assert.Equal(msg, &receivedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(events.RegisterRoomListener(roomId, backend, listener))
|
||||
defer func() {
|
||||
assert.NoError(events.UnregisterRoomListener(roomId, backend, listener))
|
||||
}()
|
||||
|
||||
roomMessage := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: &talk.BackendServerRoomRequest{
|
||||
Type: "other-test",
|
||||
},
|
||||
}
|
||||
if assert.NoError(events.PublishRoomMessage(roomId, backend, roomMessage)) {
|
||||
received := <-listener.events
|
||||
var receivedMsg AsyncMessage
|
||||
if assert.NoError(nats.Decode(received, &receivedMsg)) {
|
||||
assert.True(roomMessage.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", roomMessage.SendTime, receivedMsg.SendTime)
|
||||
receivedMsg.SendTime = roomMessage.SendTime
|
||||
assert.Equal(roomMessage, &receivedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
userId := "the-user"
|
||||
require.NoError(events.RegisterUserListener(userId, backend, listener))
|
||||
defer func() {
|
||||
assert.NoError(events.UnregisterUserListener(userId, backend, listener))
|
||||
}()
|
||||
|
||||
userMessage := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: &talk.BackendServerRoomRequest{
|
||||
Type: "user-test",
|
||||
},
|
||||
}
|
||||
if assert.NoError(events.PublishUserMessage(userId, backend, userMessage)) {
|
||||
received := <-listener.events
|
||||
var receivedMsg AsyncMessage
|
||||
if assert.NoError(nats.Decode(received, &receivedMsg)) {
|
||||
assert.True(userMessage.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", userMessage.SendTime, receivedMsg.SendTime)
|
||||
receivedMsg.SendTime = userMessage.SendTime
|
||||
assert.Equal(userMessage, &receivedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
sessionId := api.PublicSessionId("the-session")
|
||||
require.NoError(events.RegisterSessionListener(sessionId, backend, listener))
|
||||
defer func() {
|
||||
assert.NoError(events.UnregisterSessionListener(sessionId, backend, listener))
|
||||
}()
|
||||
|
||||
sessionMessage := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: &talk.BackendServerRoomRequest{
|
||||
Type: "session-test",
|
||||
},
|
||||
}
|
||||
if assert.NoError(events.PublishSessionMessage(sessionId, backend, sessionMessage)) {
|
||||
received := <-listener.events
|
||||
var receivedMsg AsyncMessage
|
||||
if assert.NoError(nats.Decode(received, &receivedMsg)) {
|
||||
assert.True(sessionMessage.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", sessionMessage.SendTime, receivedMsg.SendTime)
|
||||
receivedMsg.SendTime = sessionMessage.SendTime
|
||||
assert.Equal(sessionMessage, &receivedMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncEvents_Loopback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
events, err := NewAsyncEvents(ctx, nats.LoopbackUrl)
|
||||
require.NoError(t, err)
|
||||
testAsyncEvents(t, events)
|
||||
}
|
||||
|
||||
func TestAsyncEvents_NATS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server, _ := natstest.StartLocalServer(t)
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
events, err := NewAsyncEvents(ctx, server.ClientURL())
|
||||
require.NoError(t, err)
|
||||
testAsyncEvents(t, events)
|
||||
}
|
||||
114
async/events/test/events.go
Normal file
114
async/events/test/events.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/async/events"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/nats"
|
||||
natstest "github.com/strukturag/nextcloud-spreed-signaling/v2/nats/test"
|
||||
)
|
||||
|
||||
var (
|
||||
testTimeout = 10 * time.Second
|
||||
|
||||
EventBackendsForTest = []string{
|
||||
"loopback",
|
||||
"nats",
|
||||
}
|
||||
)
|
||||
|
||||
func GetAsyncEventsForTest(t *testing.T) events.AsyncEvents {
|
||||
var events events.AsyncEvents
|
||||
if strings.HasSuffix(t.Name(), "/nats") {
|
||||
events = getRealAsyncEventsForTest(t)
|
||||
} else {
|
||||
events = getLoopbackAsyncEventsForTest(t)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
assert.NoError(t, events.Close(ctx))
|
||||
})
|
||||
return events
|
||||
}
|
||||
|
||||
func getRealAsyncEventsForTest(t *testing.T) events.AsyncEvents {
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
server, _ := natstest.StartLocalServer(t)
|
||||
events, err := events.NewAsyncEvents(ctx, server.ClientURL())
|
||||
require.NoError(t, err)
|
||||
return events
|
||||
}
|
||||
|
||||
type natsEvents interface {
|
||||
GetNatsClient() nats.Client
|
||||
}
|
||||
|
||||
func getLoopbackAsyncEventsForTest(t *testing.T) events.AsyncEvents {
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
events, err := events.NewAsyncEvents(ctx, nats.LoopbackUrl)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
e, ok := (events.(natsEvents))
|
||||
if !ok {
|
||||
// Only can wait for NATS events.
|
||||
return
|
||||
}
|
||||
|
||||
natstest.WaitForSubscriptionsEmpty(ctx, t, e.GetNatsClient())
|
||||
})
|
||||
return events
|
||||
}
|
||||
|
||||
func WaitForAsyncEventsFlushed(ctx context.Context, t *testing.T, events events.AsyncEvents) {
|
||||
t.Helper()
|
||||
|
||||
e, ok := (events.(natsEvents))
|
||||
if !ok {
|
||||
// Only can wait for NATS events.
|
||||
return
|
||||
}
|
||||
|
||||
client, ok := e.GetNatsClient().(*nats.NativeClient)
|
||||
if !ok {
|
||||
// The loopback NATS clients is executing all events synchronously.
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, client.FlushWithContext(ctx))
|
||||
}
|
||||
94
async/events/test/events_test.go
Normal file
94
async/events/test/events_test.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/async/events"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/nats"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
type testListener struct {
|
||||
ch events.AsyncChannel
|
||||
}
|
||||
|
||||
func (l *testListener) AsyncChannel() events.AsyncChannel {
|
||||
return l.ch
|
||||
}
|
||||
|
||||
func TestAsyncEventsTest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, backend := range EventBackendsForTest {
|
||||
t.Run(backend, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
eventsHandler := GetAsyncEventsForTest(t)
|
||||
|
||||
listener := &testListener{
|
||||
ch: make(events.AsyncChannel, 1),
|
||||
}
|
||||
sessionId := api.PublicSessionId("foo")
|
||||
backend := talk.NewCompatBackend(nil)
|
||||
require.NoError(eventsHandler.RegisterSessionListener(sessionId, backend, listener))
|
||||
defer func() {
|
||||
assert.NoError(eventsHandler.UnregisterSessionListener(sessionId, backend, listener))
|
||||
}()
|
||||
|
||||
msg := events.AsyncMessage{
|
||||
Type: "message",
|
||||
Message: &api.ServerMessage{
|
||||
Type: "error",
|
||||
Error: api.NewError("test_error", "This is a test error."),
|
||||
},
|
||||
}
|
||||
if err := eventsHandler.PublishSessionMessage(sessionId, backend, &msg); assert.NoError(err) {
|
||||
select {
|
||||
case natsMsg := <-listener.ch:
|
||||
var received events.AsyncMessage
|
||||
if err := nats.Decode(natsMsg, &received); assert.NoError(err) {
|
||||
assert.True(msg.SendTime.Equal(received.SendTime), "send times don't match, expected %s, got %s", msg.SendTime, received.SendTime)
|
||||
received.SendTime = msg.SendTime
|
||||
assert.Equal(msg, received)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
require.NoError(ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
WaitForAsyncEventsFlushed(ctx, t, eventsHandler)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -19,60 +19,79 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type rootWaiter struct {
|
||||
key string
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
func (w *rootWaiter) notify() {
|
||||
close(w.ch)
|
||||
}
|
||||
|
||||
type Waiter struct {
|
||||
key string
|
||||
|
||||
sw *SingleWaiter
|
||||
ch <-chan struct{}
|
||||
}
|
||||
|
||||
func (w *Waiter) Wait(ctx context.Context) error {
|
||||
return w.sw.Wait(ctx)
|
||||
select {
|
||||
case <-w.ch:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
type Notifier struct {
|
||||
sync.Mutex
|
||||
|
||||
waiters map[string]*Waiter
|
||||
// +checklocks:Mutex
|
||||
waiters map[string]*rootWaiter
|
||||
// +checklocks:Mutex
|
||||
waiterMap map[string]map[*Waiter]bool
|
||||
}
|
||||
|
||||
func (n *Notifier) NewWaiter(key string) *Waiter {
|
||||
type ReleaseFunc func()
|
||||
|
||||
func (n *Notifier) NewWaiter(key string) (*Waiter, ReleaseFunc) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
waiter, found := n.waiters[key]
|
||||
if found {
|
||||
w := &Waiter{
|
||||
if !found {
|
||||
waiter = &rootWaiter{
|
||||
key: key,
|
||||
sw: waiter.sw,
|
||||
ch: make(chan struct{}),
|
||||
}
|
||||
|
||||
if n.waiters == nil {
|
||||
n.waiters = make(map[string]*rootWaiter)
|
||||
}
|
||||
if n.waiterMap == nil {
|
||||
n.waiterMap = make(map[string]map[*Waiter]bool)
|
||||
}
|
||||
n.waiters[key] = waiter
|
||||
if _, found := n.waiterMap[key]; !found {
|
||||
n.waiterMap[key] = make(map[*Waiter]bool)
|
||||
}
|
||||
n.waiterMap[key][w] = true
|
||||
return w
|
||||
}
|
||||
|
||||
waiter = &Waiter{
|
||||
w := &Waiter{
|
||||
key: key,
|
||||
sw: newSingleWaiter(),
|
||||
ch: waiter.ch,
|
||||
}
|
||||
if n.waiters == nil {
|
||||
n.waiters = make(map[string]*Waiter)
|
||||
n.waiterMap[key][w] = true
|
||||
releaseFunc := func() {
|
||||
n.release(w)
|
||||
}
|
||||
if n.waiterMap == nil {
|
||||
n.waiterMap = make(map[string]map[*Waiter]bool)
|
||||
}
|
||||
n.waiters[key] = waiter
|
||||
if _, found := n.waiterMap[key]; !found {
|
||||
n.waiterMap[key] = make(map[*Waiter]bool)
|
||||
}
|
||||
n.waiterMap[key][waiter] = true
|
||||
return waiter
|
||||
return w, releaseFunc
|
||||
}
|
||||
|
||||
func (n *Notifier) Reset() {
|
||||
|
|
@ -80,13 +99,13 @@ func (n *Notifier) Reset() {
|
|||
defer n.Unlock()
|
||||
|
||||
for _, w := range n.waiters {
|
||||
w.sw.cancel()
|
||||
w.notify()
|
||||
}
|
||||
n.waiters = nil
|
||||
n.waiterMap = nil
|
||||
}
|
||||
|
||||
func (n *Notifier) Release(w *Waiter) {
|
||||
func (n *Notifier) release(w *Waiter) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
|
|
@ -94,8 +113,10 @@ func (n *Notifier) Release(w *Waiter) {
|
|||
if _, found := waiters[w]; found {
|
||||
delete(waiters, w)
|
||||
if len(waiters) == 0 {
|
||||
delete(n.waiters, w.key)
|
||||
w.sw.cancel()
|
||||
if root, found := n.waiters[w.key]; found {
|
||||
delete(n.waiters, w.key)
|
||||
root.notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,7 +127,7 @@ func (n *Notifier) Notify(key string) {
|
|||
defer n.Unlock()
|
||||
|
||||
if w, found := n.waiters[key]; found {
|
||||
w.sw.cancel()
|
||||
w.notify()
|
||||
delete(n.waiters, w.key)
|
||||
delete(n.waiterMap, w.key)
|
||||
}
|
||||
|
|
@ -19,49 +19,76 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNotifierNoWaiter(t *testing.T) {
|
||||
t.Parallel()
|
||||
var notifier Notifier
|
||||
|
||||
// Notifications can be sent even if no waiter exists.
|
||||
notifier.Notify("foo")
|
||||
}
|
||||
|
||||
func TestNotifierWaitTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var notifier Notifier
|
||||
|
||||
notified := make(chan struct{})
|
||||
go func() {
|
||||
defer close(notified)
|
||||
time.Sleep(time.Second)
|
||||
notifier.Notify("foo")
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
waiter, release := notifier.NewWaiter("foo")
|
||||
defer release()
|
||||
|
||||
err := waiter.Wait(ctx)
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
<-notified
|
||||
|
||||
assert.NoError(t, waiter.Wait(t.Context()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotifierSimple(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var notifier Notifier
|
||||
waiter, release := notifier.NewWaiter("foo")
|
||||
defer release()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
waiter := notifier.NewWaiter("foo")
|
||||
defer notifier.Release(waiter)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
assert.NoError(t, waiter.Wait(ctx))
|
||||
}()
|
||||
})
|
||||
|
||||
notifier.Notify("foo")
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestNotifierMultiNotify(t *testing.T) {
|
||||
t.Parallel()
|
||||
var notifier Notifier
|
||||
|
||||
waiter := notifier.NewWaiter("foo")
|
||||
defer notifier.Release(waiter)
|
||||
_, release := notifier.NewWaiter("foo")
|
||||
defer release()
|
||||
|
||||
notifier.Notify("foo")
|
||||
// The second notification will be ignored while the first is still pending.
|
||||
|
|
@ -69,41 +96,41 @@ func TestNotifierMultiNotify(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNotifierWaitClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
var notifier Notifier
|
||||
|
||||
waiter := notifier.NewWaiter("foo")
|
||||
notifier.Release(waiter)
|
||||
waiter, release := notifier.NewWaiter("foo")
|
||||
release()
|
||||
|
||||
assert.NoError(t, waiter.Wait(context.Background()))
|
||||
}
|
||||
|
||||
func TestNotifierWaitClosedMulti(t *testing.T) {
|
||||
t.Parallel()
|
||||
var notifier Notifier
|
||||
|
||||
waiter1 := notifier.NewWaiter("foo")
|
||||
waiter2 := notifier.NewWaiter("foo")
|
||||
notifier.Release(waiter1)
|
||||
notifier.Release(waiter2)
|
||||
waiter1, release1 := notifier.NewWaiter("foo")
|
||||
waiter2, release2 := notifier.NewWaiter("foo")
|
||||
release1()
|
||||
release2()
|
||||
|
||||
assert.NoError(t, waiter1.Wait(context.Background()))
|
||||
assert.NoError(t, waiter2.Wait(context.Background()))
|
||||
}
|
||||
|
||||
func TestNotifierResetWillNotify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var notifier Notifier
|
||||
waiter, release := notifier.NewWaiter("foo")
|
||||
defer release()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
waiter := notifier.NewWaiter("foo")
|
||||
defer notifier.Release(waiter)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
assert.NoError(t, waiter.Wait(ctx))
|
||||
}()
|
||||
})
|
||||
|
||||
notifier.Reset()
|
||||
wg.Wait()
|
||||
|
|
@ -111,31 +138,24 @@ func TestNotifierResetWillNotify(t *testing.T) {
|
|||
|
||||
func TestNotifierDuplicate(t *testing.T) {
|
||||
t.Parallel()
|
||||
var notifier Notifier
|
||||
var wgStart sync.WaitGroup
|
||||
var wgEnd sync.WaitGroup
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var notifier Notifier
|
||||
var done sync.WaitGroup
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
wgStart.Add(1)
|
||||
wgEnd.Add(1)
|
||||
for range 2 {
|
||||
done.Go(func() {
|
||||
waiter, release := notifier.NewWaiter("foo")
|
||||
defer release()
|
||||
|
||||
go func() {
|
||||
defer wgEnd.Done()
|
||||
waiter := notifier.NewWaiter("foo")
|
||||
defer notifier.Release(waiter)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
assert.NoError(t, waiter.Wait(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// Goroutine has created the waiter and is ready.
|
||||
wgStart.Done()
|
||||
synctest.Wait()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
assert.NoError(t, waiter.Wait(ctx))
|
||||
}()
|
||||
}
|
||||
|
||||
wgStart.Wait()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
notifier.Notify("foo")
|
||||
wgEnd.Wait()
|
||||
notifier.Notify("foo")
|
||||
done.Wait()
|
||||
})
|
||||
}
|
||||
|
|
@ -19,16 +19,18 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -90,25 +92,33 @@ type throttleEntry struct {
|
|||
ts time.Time
|
||||
}
|
||||
|
||||
type memoryThrottler struct {
|
||||
getNow func() time.Time
|
||||
doDelay func(context.Context, time.Duration)
|
||||
type GetTimeFunc func() time.Time
|
||||
type ThrottleDelayFunc func(context.Context, time.Duration)
|
||||
|
||||
mu sync.RWMutex
|
||||
type memoryThrottler struct {
|
||||
getNow GetTimeFunc
|
||||
doDelay ThrottleDelayFunc
|
||||
|
||||
mu sync.RWMutex
|
||||
// +checklocks:mu
|
||||
clients map[string]map[string][]throttleEntry
|
||||
|
||||
closer *Closer
|
||||
closer *internal.Closer
|
||||
}
|
||||
|
||||
func NewMemoryThrottler() (Throttler, error) {
|
||||
return NewCustomMemoryThrottler(time.Now, defaultDelay)
|
||||
}
|
||||
|
||||
func NewCustomMemoryThrottler(getNow GetTimeFunc, delay ThrottleDelayFunc) (Throttler, error) {
|
||||
result := &memoryThrottler{
|
||||
getNow: time.Now,
|
||||
getNow: getNow,
|
||||
doDelay: delay,
|
||||
|
||||
clients: make(map[string]map[string][]throttleEntry),
|
||||
|
||||
closer: NewCloser(),
|
||||
closer: internal.NewCloser(),
|
||||
}
|
||||
result.doDelay = result.delay
|
||||
go result.housekeeping()
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -257,10 +267,7 @@ func (t *memoryThrottler) getDelay(count int) time.Duration {
|
|||
return maxThrottleDelay
|
||||
}
|
||||
|
||||
delay := time.Duration(100*intPow(2, count)) * time.Millisecond
|
||||
if delay > maxThrottleDelay {
|
||||
delay = maxThrottleDelay
|
||||
}
|
||||
delay := min(time.Duration(100*intPow(2, count))*time.Millisecond, maxThrottleDelay)
|
||||
return delay
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +286,8 @@ func (t *memoryThrottler) CheckBruteforce(ctx context.Context, client string, ac
|
|||
if l >= maxBruteforceAttempts {
|
||||
delta := now.Sub(entries[l-maxBruteforceAttempts].ts)
|
||||
if delta <= maxBruteforceDurationThreshold {
|
||||
log.Printf("Detected bruteforce attempt on \"%s\" from %s", action, client)
|
||||
logger := log.LoggerFromContext(ctx)
|
||||
logger.Printf("Detected bruteforce attempt on \"%s\" from %s", action, client)
|
||||
statsThrottleBruteforceTotal.WithLabelValues(action).Inc()
|
||||
return doThrottle, ErrBruteforceDetected
|
||||
}
|
||||
|
|
@ -303,12 +311,13 @@ func (t *memoryThrottler) throttle(ctx context.Context, client string, action st
|
|||
}
|
||||
count := t.addEntry(client, action, entry)
|
||||
delay := t.getDelay(count - 1)
|
||||
log.Printf("Failed attempt on \"%s\" from %s, throttling by %s", action, client, delay)
|
||||
logger := log.LoggerFromContext(ctx)
|
||||
logger.Printf("Failed attempt on \"%s\" from %s, throttling by %s", action, client, delay)
|
||||
statsThrottleDelayedTotal.WithLabelValues(action, strconv.FormatInt(delay.Milliseconds(), 10)).Inc()
|
||||
t.doDelay(ctx, delay)
|
||||
}
|
||||
|
||||
func (t *memoryThrottler) delay(ctx context.Context, duration time.Duration) {
|
||||
func defaultDelay(ctx context.Context, duration time.Duration) {
|
||||
c, cancel := context.WithTimeout(ctx, duration)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -19,10 +19,12 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package async
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -47,5 +49,5 @@ var (
|
|||
)
|
||||
|
||||
func RegisterThrottleStats() {
|
||||
registerAll(throttleStats...)
|
||||
metrics.RegisterAll(throttleStats...)
|
||||
}
|
||||
303
async/throttle_test.go
Normal file
303
async/throttle_test.go
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2024 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package async
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
)
|
||||
|
||||
func newMemoryThrottlerForTest(t *testing.T) Throttler {
|
||||
t.Helper()
|
||||
result, err := NewMemoryThrottler()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
result.Close()
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func expectDelay(t *testing.T, f func(), delay time.Duration) {
|
||||
t.Helper()
|
||||
a := time.Now()
|
||||
f()
|
||||
b := time.Now()
|
||||
assert.Equal(t, delay, b.Sub(a))
|
||||
}
|
||||
|
||||
func TestThrottler(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := newMemoryThrottlerForTest(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle1(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle2(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
|
||||
throttle3, err := th.CheckBruteforce(ctx, "192.168.0.2", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle3(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
throttle4, err := th.CheckBruteforce(ctx, "192.168.0.1", "action2")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle4(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThrottlerIPv6(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := newMemoryThrottlerForTest(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
// Make sure full /64 subnets are throttled for IPv6.
|
||||
throttle1, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle1(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
throttle2, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::2", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle2(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
|
||||
// A diffent /64 subnet is not throttled yet.
|
||||
throttle3, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0013::1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle3(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
// A different action is not throttled.
|
||||
throttle4, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::1", "action2")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle4(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThrottler_Bruteforce(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := newMemoryThrottlerForTest(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
delay := 100 * time.Millisecond
|
||||
for range maxBruteforceAttempts {
|
||||
throttle, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle(ctx)
|
||||
}, delay)
|
||||
delay *= 2
|
||||
if delay > maxThrottleDelay {
|
||||
delay = maxThrottleDelay
|
||||
}
|
||||
}
|
||||
|
||||
_, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.ErrorIs(err, ErrBruteforceDetected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThrottler_Cleanup(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
throttler := newMemoryThrottlerForTest(t)
|
||||
th, ok := throttler.(*memoryThrottler)
|
||||
require.True(t, ok, "required memoryThrottler, got %T", throttler)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle1(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
throttle2, err := th.CheckBruteforce(ctx, "192.168.0.2", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle2(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
time.Sleep(time.Hour)
|
||||
|
||||
throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action2")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle3(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
throttle4, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle4(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
|
||||
cleanupNow := time.Now().Add(-time.Hour).Add(maxBruteforceAge).Add(time.Second)
|
||||
th.cleanup(cleanupNow)
|
||||
|
||||
assert.Len(th.getEntries("192.168.0.1", "action1"), 1)
|
||||
assert.Len(th.getEntries("192.168.0.1", "action2"), 1)
|
||||
|
||||
th.mu.RLock()
|
||||
if entries, found := th.clients["192.168.0.2"]; found {
|
||||
assert.Fail("should have removed client \"192.168.0.2\"", "got %+v", entries)
|
||||
}
|
||||
th.mu.RUnlock()
|
||||
|
||||
throttle5, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle5(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThrottler_ExpirePartial(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := newMemoryThrottlerForTest(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle1(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
time.Sleep(time.Minute)
|
||||
|
||||
throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle2(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
|
||||
time.Sleep(maxBruteforceAge - time.Minute + time.Second)
|
||||
|
||||
throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle3(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThrottler_ExpireAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := newMemoryThrottlerForTest(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle1(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle2(ctx)
|
||||
}, 200*time.Millisecond)
|
||||
|
||||
time.Sleep(maxBruteforceAge + time.Second)
|
||||
|
||||
throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
assert.NoError(err)
|
||||
expectDelay(t, func() {
|
||||
throttle3(ctx)
|
||||
}, 100*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThrottler_Negative(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := newMemoryThrottlerForTest(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
ctx := log.NewLoggerContext(t.Context(), logger)
|
||||
|
||||
delay := 100 * time.Millisecond
|
||||
for range maxBruteforceAttempts * 10 {
|
||||
throttle, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1")
|
||||
if err != nil {
|
||||
assert.ErrorIs(err, ErrBruteforceDetected)
|
||||
}
|
||||
expectDelay(t, func() {
|
||||
throttle(ctx)
|
||||
}, delay)
|
||||
delay *= 2
|
||||
if delay > maxThrottleDelay {
|
||||
delay = maxThrottleDelay
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
210
async_events.go
210
async_events.go
|
|
@ -1,210 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import "sync"
|
||||
|
||||
type AsyncBackendRoomEventListener interface {
|
||||
ProcessBackendRoomRequest(message *AsyncMessage)
|
||||
}
|
||||
|
||||
type AsyncRoomEventListener interface {
|
||||
ProcessAsyncRoomMessage(message *AsyncMessage)
|
||||
}
|
||||
|
||||
type AsyncUserEventListener interface {
|
||||
ProcessAsyncUserMessage(message *AsyncMessage)
|
||||
}
|
||||
|
||||
type AsyncSessionEventListener interface {
|
||||
ProcessAsyncSessionMessage(message *AsyncMessage)
|
||||
}
|
||||
|
||||
type AsyncEvents interface {
|
||||
Close()
|
||||
|
||||
RegisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) error
|
||||
UnregisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener)
|
||||
|
||||
RegisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) error
|
||||
UnregisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener)
|
||||
|
||||
RegisterUserListener(userId string, backend *Backend, listener AsyncUserEventListener) error
|
||||
UnregisterUserListener(userId string, backend *Backend, listener AsyncUserEventListener)
|
||||
|
||||
RegisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) error
|
||||
UnregisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener)
|
||||
|
||||
PublishBackendRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error
|
||||
PublishRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error
|
||||
PublishUserMessage(userId string, backend *Backend, message *AsyncMessage) error
|
||||
PublishSessionMessage(sessionId string, backend *Backend, message *AsyncMessage) error
|
||||
}
|
||||
|
||||
func NewAsyncEvents(url string) (AsyncEvents, error) {
|
||||
client, err := NewNatsClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewAsyncEventsNats(client)
|
||||
}
|
||||
|
||||
type asyncBackendRoomSubscriber struct {
|
||||
mu sync.Mutex
|
||||
|
||||
listeners map[AsyncBackendRoomEventListener]bool
|
||||
}
|
||||
|
||||
func (s *asyncBackendRoomSubscriber) processBackendRoomRequest(message *AsyncMessage) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for listener := range s.listeners {
|
||||
s.mu.Unlock()
|
||||
listener.ProcessBackendRoomRequest(message)
|
||||
s.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *asyncBackendRoomSubscriber) addListener(listener AsyncBackendRoomEventListener) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[AsyncBackendRoomEventListener]bool)
|
||||
}
|
||||
s.listeners[listener] = true
|
||||
}
|
||||
|
||||
func (s *asyncBackendRoomSubscriber) removeListener(listener AsyncBackendRoomEventListener) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.listeners, listener)
|
||||
return len(s.listeners) > 0
|
||||
}
|
||||
|
||||
type asyncRoomSubscriber struct {
|
||||
mu sync.Mutex
|
||||
|
||||
listeners map[AsyncRoomEventListener]bool
|
||||
}
|
||||
|
||||
func (s *asyncRoomSubscriber) processAsyncRoomMessage(message *AsyncMessage) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for listener := range s.listeners {
|
||||
s.mu.Unlock()
|
||||
listener.ProcessAsyncRoomMessage(message)
|
||||
s.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *asyncRoomSubscriber) addListener(listener AsyncRoomEventListener) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[AsyncRoomEventListener]bool)
|
||||
}
|
||||
s.listeners[listener] = true
|
||||
}
|
||||
|
||||
func (s *asyncRoomSubscriber) removeListener(listener AsyncRoomEventListener) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.listeners, listener)
|
||||
return len(s.listeners) > 0
|
||||
}
|
||||
|
||||
type asyncUserSubscriber struct {
|
||||
mu sync.Mutex
|
||||
|
||||
listeners map[AsyncUserEventListener]bool
|
||||
}
|
||||
|
||||
func (s *asyncUserSubscriber) processAsyncUserMessage(message *AsyncMessage) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for listener := range s.listeners {
|
||||
s.mu.Unlock()
|
||||
listener.ProcessAsyncUserMessage(message)
|
||||
s.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *asyncUserSubscriber) addListener(listener AsyncUserEventListener) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[AsyncUserEventListener]bool)
|
||||
}
|
||||
s.listeners[listener] = true
|
||||
}
|
||||
|
||||
func (s *asyncUserSubscriber) removeListener(listener AsyncUserEventListener) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.listeners, listener)
|
||||
return len(s.listeners) > 0
|
||||
}
|
||||
|
||||
type asyncSessionSubscriber struct {
|
||||
mu sync.Mutex
|
||||
|
||||
listeners map[AsyncSessionEventListener]bool
|
||||
}
|
||||
|
||||
func (s *asyncSessionSubscriber) processAsyncSessionMessage(message *AsyncMessage) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for listener := range s.listeners {
|
||||
s.mu.Unlock()
|
||||
listener.ProcessAsyncSessionMessage(message)
|
||||
s.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *asyncSessionSubscriber) addListener(listener AsyncSessionEventListener) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[AsyncSessionEventListener]bool)
|
||||
}
|
||||
s.listeners[listener] = true
|
||||
}
|
||||
|
||||
func (s *asyncSessionSubscriber) removeListener(listener AsyncSessionEventListener) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.listeners, listener)
|
||||
return len(s.listeners) > 0
|
||||
}
|
||||
|
|
@ -1,452 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
func GetSubjectForBackendRoomId(roomId string, backend *Backend) string {
|
||||
if backend == nil || backend.IsCompat() {
|
||||
return GetEncodedSubject("backend.room", roomId)
|
||||
}
|
||||
|
||||
return GetEncodedSubject("backend.room", roomId+"|"+backend.Id())
|
||||
}
|
||||
|
||||
func GetSubjectForRoomId(roomId string, backend *Backend) string {
|
||||
if backend == nil || backend.IsCompat() {
|
||||
return GetEncodedSubject("room", roomId)
|
||||
}
|
||||
|
||||
return GetEncodedSubject("room", roomId+"|"+backend.Id())
|
||||
}
|
||||
|
||||
func GetSubjectForUserId(userId string, backend *Backend) string {
|
||||
if backend == nil || backend.IsCompat() {
|
||||
return GetEncodedSubject("user", userId)
|
||||
}
|
||||
|
||||
return GetEncodedSubject("user", userId+"|"+backend.Id())
|
||||
}
|
||||
|
||||
func GetSubjectForSessionId(sessionId string, backend *Backend) string {
|
||||
return "session." + sessionId
|
||||
}
|
||||
|
||||
type asyncSubscriberNats struct {
|
||||
key string
|
||||
client NatsClient
|
||||
|
||||
receiver chan *nats.Msg
|
||||
closeChan chan struct{}
|
||||
subscription NatsSubscription
|
||||
|
||||
processMessage func(*nats.Msg)
|
||||
}
|
||||
|
||||
func newAsyncSubscriberNats(key string, client NatsClient) (*asyncSubscriberNats, error) {
|
||||
receiver := make(chan *nats.Msg, 64)
|
||||
sub, err := client.Subscribe(key, receiver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &asyncSubscriberNats{
|
||||
key: key,
|
||||
client: client,
|
||||
|
||||
receiver: receiver,
|
||||
closeChan: make(chan struct{}),
|
||||
subscription: sub,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *asyncSubscriberNats) run() {
|
||||
defer func() {
|
||||
if err := s.subscription.Unsubscribe(); err != nil {
|
||||
log.Printf("Error unsubscribing %s: %s", s.key, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.receiver:
|
||||
s.processMessage(msg)
|
||||
for count := len(s.receiver); count > 0; count-- {
|
||||
s.processMessage(<-s.receiver)
|
||||
}
|
||||
case <-s.closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *asyncSubscriberNats) close() {
|
||||
close(s.closeChan)
|
||||
}
|
||||
|
||||
type asyncBackendRoomSubscriberNats struct {
|
||||
*asyncSubscriberNats
|
||||
asyncBackendRoomSubscriber
|
||||
}
|
||||
|
||||
func newAsyncBackendRoomSubscriberNats(key string, client NatsClient) (*asyncBackendRoomSubscriberNats, error) {
|
||||
sub, err := newAsyncSubscriberNats(key, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &asyncBackendRoomSubscriberNats{
|
||||
asyncSubscriberNats: sub,
|
||||
}
|
||||
result.processMessage = result.doProcessMessage
|
||||
go result.run()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *asyncBackendRoomSubscriberNats) doProcessMessage(msg *nats.Msg) {
|
||||
var message AsyncMessage
|
||||
if err := s.client.Decode(msg, &message); err != nil {
|
||||
log.Printf("Could not decode NATS message %+v, %s", msg, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.processBackendRoomRequest(&message)
|
||||
}
|
||||
|
||||
type asyncRoomSubscriberNats struct {
|
||||
asyncRoomSubscriber
|
||||
*asyncSubscriberNats
|
||||
}
|
||||
|
||||
func newAsyncRoomSubscriberNats(key string, client NatsClient) (*asyncRoomSubscriberNats, error) {
|
||||
sub, err := newAsyncSubscriberNats(key, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &asyncRoomSubscriberNats{
|
||||
asyncSubscriberNats: sub,
|
||||
}
|
||||
result.processMessage = result.doProcessMessage
|
||||
go result.run()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *asyncRoomSubscriberNats) doProcessMessage(msg *nats.Msg) {
|
||||
var message AsyncMessage
|
||||
if err := s.client.Decode(msg, &message); err != nil {
|
||||
log.Printf("Could not decode nats message %+v, %s", msg, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.processAsyncRoomMessage(&message)
|
||||
}
|
||||
|
||||
type asyncUserSubscriberNats struct {
|
||||
*asyncSubscriberNats
|
||||
asyncUserSubscriber
|
||||
}
|
||||
|
||||
func newAsyncUserSubscriberNats(key string, client NatsClient) (*asyncUserSubscriberNats, error) {
|
||||
sub, err := newAsyncSubscriberNats(key, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &asyncUserSubscriberNats{
|
||||
asyncSubscriberNats: sub,
|
||||
}
|
||||
result.processMessage = result.doProcessMessage
|
||||
go result.run()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *asyncUserSubscriberNats) doProcessMessage(msg *nats.Msg) {
|
||||
var message AsyncMessage
|
||||
if err := s.client.Decode(msg, &message); err != nil {
|
||||
log.Printf("Could not decode nats message %+v, %s", msg, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.processAsyncUserMessage(&message)
|
||||
}
|
||||
|
||||
type asyncSessionSubscriberNats struct {
|
||||
*asyncSubscriberNats
|
||||
asyncSessionSubscriber
|
||||
}
|
||||
|
||||
func newAsyncSessionSubscriberNats(key string, client NatsClient) (*asyncSessionSubscriberNats, error) {
|
||||
sub, err := newAsyncSubscriberNats(key, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &asyncSessionSubscriberNats{
|
||||
asyncSubscriberNats: sub,
|
||||
}
|
||||
result.processMessage = result.doProcessMessage
|
||||
go result.run()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *asyncSessionSubscriberNats) doProcessMessage(msg *nats.Msg) {
|
||||
var message AsyncMessage
|
||||
if err := s.client.Decode(msg, &message); err != nil {
|
||||
log.Printf("Could not decode nats message %+v, %s", msg, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.processAsyncSessionMessage(&message)
|
||||
}
|
||||
|
||||
type asyncEventsNats struct {
|
||||
mu sync.Mutex
|
||||
client NatsClient
|
||||
|
||||
backendRoomSubscriptions map[string]*asyncBackendRoomSubscriberNats
|
||||
roomSubscriptions map[string]*asyncRoomSubscriberNats
|
||||
userSubscriptions map[string]*asyncUserSubscriberNats
|
||||
sessionSubscriptions map[string]*asyncSessionSubscriberNats
|
||||
}
|
||||
|
||||
func NewAsyncEventsNats(client NatsClient) (AsyncEvents, error) {
|
||||
events := &asyncEventsNats{
|
||||
client: client,
|
||||
|
||||
backendRoomSubscriptions: make(map[string]*asyncBackendRoomSubscriberNats),
|
||||
roomSubscriptions: make(map[string]*asyncRoomSubscriberNats),
|
||||
userSubscriptions: make(map[string]*asyncUserSubscriberNats),
|
||||
sessionSubscriptions: make(map[string]*asyncSessionSubscriberNats),
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) Close() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func(subscriptions map[string]*asyncBackendRoomSubscriberNats) {
|
||||
defer wg.Done()
|
||||
for _, sub := range subscriptions {
|
||||
sub.close()
|
||||
}
|
||||
}(e.backendRoomSubscriptions)
|
||||
wg.Add(1)
|
||||
go func(subscriptions map[string]*asyncRoomSubscriberNats) {
|
||||
defer wg.Done()
|
||||
for _, sub := range subscriptions {
|
||||
sub.close()
|
||||
}
|
||||
}(e.roomSubscriptions)
|
||||
wg.Add(1)
|
||||
go func(subscriptions map[string]*asyncUserSubscriberNats) {
|
||||
defer wg.Done()
|
||||
for _, sub := range subscriptions {
|
||||
sub.close()
|
||||
}
|
||||
}(e.userSubscriptions)
|
||||
wg.Add(1)
|
||||
go func(subscriptions map[string]*asyncSessionSubscriberNats) {
|
||||
defer wg.Done()
|
||||
for _, sub := range subscriptions {
|
||||
sub.close()
|
||||
}
|
||||
}(e.sessionSubscriptions)
|
||||
// Can't use clear(...) here as the maps are processed asynchronously by the
|
||||
// goroutines above.
|
||||
e.backendRoomSubscriptions = make(map[string]*asyncBackendRoomSubscriberNats)
|
||||
e.roomSubscriptions = make(map[string]*asyncRoomSubscriberNats)
|
||||
e.userSubscriptions = make(map[string]*asyncUserSubscriberNats)
|
||||
e.sessionSubscriptions = make(map[string]*asyncSessionSubscriberNats)
|
||||
wg.Wait()
|
||||
e.client.Close()
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) error {
|
||||
key := GetSubjectForBackendRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.backendRoomSubscriptions[key]
|
||||
if !found {
|
||||
var err error
|
||||
if sub, err = newAsyncBackendRoomSubscriberNats(key, e.client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.backendRoomSubscriptions[key] = sub
|
||||
}
|
||||
sub.addListener(listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) {
|
||||
key := GetSubjectForBackendRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.backendRoomSubscriptions[key]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
if !sub.removeListener(listener) {
|
||||
delete(e.backendRoomSubscriptions, key)
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) error {
|
||||
key := GetSubjectForRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.roomSubscriptions[key]
|
||||
if !found {
|
||||
var err error
|
||||
if sub, err = newAsyncRoomSubscriberNats(key, e.client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.roomSubscriptions[key] = sub
|
||||
}
|
||||
sub.addListener(listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) {
|
||||
key := GetSubjectForRoomId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.roomSubscriptions[key]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
if !sub.removeListener(listener) {
|
||||
delete(e.roomSubscriptions, key)
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterUserListener(roomId string, backend *Backend, listener AsyncUserEventListener) error {
|
||||
key := GetSubjectForUserId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.userSubscriptions[key]
|
||||
if !found {
|
||||
var err error
|
||||
if sub, err = newAsyncUserSubscriberNats(key, e.client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.userSubscriptions[key] = sub
|
||||
}
|
||||
sub.addListener(listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterUserListener(roomId string, backend *Backend, listener AsyncUserEventListener) {
|
||||
key := GetSubjectForUserId(roomId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.userSubscriptions[key]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
if !sub.removeListener(listener) {
|
||||
delete(e.userSubscriptions, key)
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) RegisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) error {
|
||||
key := GetSubjectForSessionId(sessionId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.sessionSubscriptions[key]
|
||||
if !found {
|
||||
var err error
|
||||
if sub, err = newAsyncSessionSubscriberNats(key, e.client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.sessionSubscriptions[key] = sub
|
||||
}
|
||||
sub.addListener(listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) UnregisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) {
|
||||
key := GetSubjectForSessionId(sessionId, backend)
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
sub, found := e.sessionSubscriptions[key]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
if !sub.removeListener(listener) {
|
||||
delete(e.sessionSubscriptions, key)
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) publish(subject string, message *AsyncMessage) error {
|
||||
message.SendTime = time.Now()
|
||||
return e.client.Publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishBackendRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForBackendRoomId(roomId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForRoomId(roomId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishUserMessage(userId string, backend *Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForUserId(userId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
||||
func (e *asyncEventsNats) PublishSessionMessage(sessionId string, backend *Backend, message *AsyncMessage) error {
|
||||
subject := GetSubjectForSessionId(sessionId, backend)
|
||||
return e.publish(subject, message)
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
eventBackendsForTest = []string{
|
||||
"loopback",
|
||||
"nats",
|
||||
}
|
||||
)
|
||||
|
||||
func getAsyncEventsForTest(t *testing.T) AsyncEvents {
|
||||
var events AsyncEvents
|
||||
if strings.HasSuffix(t.Name(), "/nats") {
|
||||
events = getRealAsyncEventsForTest(t)
|
||||
} else {
|
||||
events = getLoopbackAsyncEventsForTest(t)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
events.Close()
|
||||
})
|
||||
return events
|
||||
}
|
||||
|
||||
func getRealAsyncEventsForTest(t *testing.T) AsyncEvents {
|
||||
url := startLocalNatsServer(t)
|
||||
events, err := NewAsyncEvents(url)
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func getLoopbackAsyncEventsForTest(t *testing.T) AsyncEvents {
|
||||
events, err := NewAsyncEvents(NatsLoopbackUrl)
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
nats := (events.(*asyncEventsNats)).client
|
||||
(nats).(*LoopbackNatsClient).waitForSubscriptionsEmpty(ctx, t)
|
||||
})
|
||||
return events
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2017 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotRedirecting = errors.New("not redirecting to different host")
|
||||
ErrUnsupportedContentType = errors.New("unsupported_content_type")
|
||||
|
||||
ErrIncompleteResponse = errors.New("incomplete OCS response")
|
||||
ErrThrottledResponse = errors.New("throttled OCS response")
|
||||
)
|
||||
|
||||
type BackendClient struct {
|
||||
hub *Hub
|
||||
version string
|
||||
backends *BackendConfiguration
|
||||
|
||||
pool *HttpClientPool
|
||||
capabilities *Capabilities
|
||||
}
|
||||
|
||||
func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost int, version string, etcdClient *EtcdClient) (*BackendClient, error) {
|
||||
backends, err := NewBackendConfiguration(config, etcdClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skipverify, _ := config.GetBool("backend", "skipverify")
|
||||
if skipverify {
|
||||
log.Println("WARNING: Backend verification is disabled!")
|
||||
}
|
||||
|
||||
pool, err := NewHttpClientPool(maxConcurrentRequestsPerHost, skipverify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
capabilities, err := NewCapabilities(version, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BackendClient{
|
||||
version: version,
|
||||
backends: backends,
|
||||
|
||||
pool: pool,
|
||||
capabilities: capabilities,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BackendClient) Close() {
|
||||
b.backends.Close()
|
||||
}
|
||||
|
||||
func (b *BackendClient) Reload(config *goconf.ConfigFile) {
|
||||
b.backends.Reload(config)
|
||||
}
|
||||
|
||||
func (b *BackendClient) GetCompatBackend() *Backend {
|
||||
return b.backends.GetCompatBackend()
|
||||
}
|
||||
|
||||
func (b *BackendClient) GetBackend(u *url.URL) *Backend {
|
||||
return b.backends.GetBackend(u)
|
||||
}
|
||||
|
||||
func (b *BackendClient) GetBackends() []*Backend {
|
||||
return b.backends.GetBackends()
|
||||
}
|
||||
|
||||
func (b *BackendClient) IsUrlAllowed(u *url.URL) bool {
|
||||
return b.backends.IsUrlAllowed(u)
|
||||
}
|
||||
|
||||
func isOcsRequest(u *url.URL) bool {
|
||||
return strings.Contains(u.Path, "/ocs/v2.php") || strings.Contains(u.Path, "/ocs/v1.php")
|
||||
}
|
||||
|
||||
// PerformJSONRequest sends a JSON POST request to the given url and decodes
|
||||
// the result into "response".
|
||||
func (b *BackendClient) PerformJSONRequest(ctx context.Context, u *url.URL, request interface{}, response interface{}) error {
|
||||
if u == nil {
|
||||
return fmt.Errorf("no url passed to perform JSON request %+v", request)
|
||||
}
|
||||
|
||||
secret := b.backends.GetSecret(u)
|
||||
if secret == nil {
|
||||
return fmt.Errorf("no backend secret configured for for %s", u)
|
||||
}
|
||||
|
||||
var requestUrl *url.URL
|
||||
if b.capabilities.HasCapabilityFeature(ctx, u, FeatureSignalingV3Api) {
|
||||
newUrl := *u
|
||||
newUrl.Path = strings.Replace(newUrl.Path, "/spreed/api/v1/signaling/", "/spreed/api/v3/signaling/", -1)
|
||||
newUrl.Path = strings.Replace(newUrl.Path, "/spreed/api/v2/signaling/", "/spreed/api/v3/signaling/", -1)
|
||||
requestUrl = &newUrl
|
||||
} else {
|
||||
requestUrl = u
|
||||
}
|
||||
|
||||
c, pool, err := b.pool.Get(ctx, u)
|
||||
if err != nil {
|
||||
log.Printf("Could not get client for host %s: %s", u.Host, err)
|
||||
return err
|
||||
}
|
||||
defer pool.Put(c)
|
||||
|
||||
data, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
log.Printf("Could not marshal request %+v: %s", request, err)
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", requestUrl.String(), bytes.NewReader(data))
|
||||
if err != nil {
|
||||
log.Printf("Could not create request to %s: %s", requestUrl, err)
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("OCS-APIRequest", "true")
|
||||
req.Header.Set("User-Agent", "nextcloud-spreed-signaling/"+b.version)
|
||||
if b.hub != nil {
|
||||
req.Header.Set("X-Spreed-Signaling-Features", strings.Join(b.hub.info.Features, ", "))
|
||||
}
|
||||
|
||||
// Add checksum so the backend can validate the request.
|
||||
AddBackendChecksum(req, data, secret)
|
||||
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Could not send request %s to %s: %s", string(data), req.URL, err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, "application/json") {
|
||||
log.Printf("Received unsupported content-type from %s: %s (%s)", req.URL, ct, resp.Status)
|
||||
return ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("Could not read response body from %s: %s", req.URL, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if isOcsRequest(u) || req.Header.Get("OCS-APIRequest") != "" {
|
||||
// OCS response are wrapped in an OCS container that needs to be parsed
|
||||
// to get the actual contents:
|
||||
// {
|
||||
// "ocs": {
|
||||
// "meta": { ... },
|
||||
// "data": { ... }
|
||||
// }
|
||||
// }
|
||||
var ocs OcsResponse
|
||||
if err := json.Unmarshal(body, &ocs); err != nil {
|
||||
log.Printf("Could not decode OCS response %s from %s: %s", string(body), req.URL, err)
|
||||
return err
|
||||
} else if ocs.Ocs == nil || len(ocs.Ocs.Data) == 0 {
|
||||
log.Printf("Incomplete OCS response %s from %s", string(body), req.URL)
|
||||
return ErrIncompleteResponse
|
||||
}
|
||||
|
||||
switch ocs.Ocs.Meta.StatusCode {
|
||||
case http.StatusTooManyRequests:
|
||||
log.Printf("Throttled OCS response %s from %s", string(body), req.URL)
|
||||
return ErrThrottledResponse
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(ocs.Ocs.Data, response); err != nil {
|
||||
log.Printf("Could not decode OCS response body %s from %s: %s", string(ocs.Ocs.Data), req.URL, err)
|
||||
return err
|
||||
}
|
||||
} else if err := json.Unmarshal(body, response); err != nil {
|
||||
log.Printf("Could not decode response body %s from %s: %s", string(body), req.URL, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
)
|
||||
|
||||
type backendStorageStatic struct {
|
||||
backendStorageCommon
|
||||
|
||||
// Deprecated
|
||||
allowAll bool
|
||||
commonSecret []byte
|
||||
compatBackend *Backend
|
||||
}
|
||||
|
||||
func NewBackendStorageStatic(config *goconf.ConfigFile) (BackendStorage, error) {
|
||||
allowAll, _ := config.GetBool("backend", "allowall")
|
||||
allowHttp, _ := config.GetBool("backend", "allowhttp")
|
||||
commonSecret, _ := config.GetString("backend", "secret")
|
||||
sessionLimit, err := config.GetInt("backend", "sessionlimit")
|
||||
if err != nil || sessionLimit < 0 {
|
||||
sessionLimit = 0
|
||||
}
|
||||
backends := make(map[string][]*Backend)
|
||||
var compatBackend *Backend
|
||||
numBackends := 0
|
||||
if allowAll {
|
||||
log.Println("WARNING: All backend hostnames are allowed, only use for development!")
|
||||
compatBackend = &Backend{
|
||||
id: "compat",
|
||||
secret: []byte(commonSecret),
|
||||
compat: true,
|
||||
|
||||
allowHttp: allowHttp,
|
||||
|
||||
sessionLimit: uint64(sessionLimit),
|
||||
}
|
||||
if sessionLimit > 0 {
|
||||
log.Printf("Allow a maximum of %d sessions", sessionLimit)
|
||||
}
|
||||
updateBackendStats(compatBackend)
|
||||
numBackends++
|
||||
} else if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" {
|
||||
for host, configuredBackends := range getConfiguredHosts(backendIds, config, commonSecret) {
|
||||
backends[host] = append(backends[host], configuredBackends...)
|
||||
for _, be := range configuredBackends {
|
||||
log.Printf("Backend %s added for %s", be.id, be.url)
|
||||
updateBackendStats(be)
|
||||
}
|
||||
numBackends += len(configuredBackends)
|
||||
}
|
||||
} else if allowedUrls, _ := config.GetString("backend", "allowed"); allowedUrls != "" {
|
||||
// Old-style configuration, only hosts are configured and are using a common secret.
|
||||
allowMap := make(map[string]bool)
|
||||
for _, u := range strings.Split(allowedUrls, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if idx := strings.IndexByte(u, '/'); idx != -1 {
|
||||
log.Printf("WARNING: Removing path from allowed hostname \"%s\", check your configuration!", u)
|
||||
u = u[:idx]
|
||||
}
|
||||
if u != "" {
|
||||
allowMap[strings.ToLower(u)] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowMap) == 0 {
|
||||
log.Println("WARNING: No backend hostnames are allowed, check your configuration!")
|
||||
} else {
|
||||
compatBackend = &Backend{
|
||||
id: "compat",
|
||||
secret: []byte(commonSecret),
|
||||
compat: true,
|
||||
|
||||
allowHttp: allowHttp,
|
||||
|
||||
sessionLimit: uint64(sessionLimit),
|
||||
}
|
||||
hosts := make([]string, 0, len(allowMap))
|
||||
for host := range allowMap {
|
||||
hosts = append(hosts, host)
|
||||
backends[host] = []*Backend{compatBackend}
|
||||
}
|
||||
if len(hosts) > 1 {
|
||||
log.Println("WARNING: Using deprecated backend configuration. Please migrate the \"allowed\" setting to the new \"backends\" configuration.")
|
||||
}
|
||||
log.Printf("Allowed backend hostnames: %s", hosts)
|
||||
if sessionLimit > 0 {
|
||||
log.Printf("Allow a maximum of %d sessions", sessionLimit)
|
||||
}
|
||||
updateBackendStats(compatBackend)
|
||||
numBackends++
|
||||
}
|
||||
}
|
||||
|
||||
if numBackends == 0 {
|
||||
log.Printf("WARNING: No backends configured, client connections will not be possible.")
|
||||
}
|
||||
|
||||
statsBackendsCurrent.Add(float64(numBackends))
|
||||
return &backendStorageStatic{
|
||||
backendStorageCommon: backendStorageCommon{
|
||||
backends: backends,
|
||||
},
|
||||
|
||||
allowAll: allowAll,
|
||||
commonSecret: []byte(commonSecret),
|
||||
compatBackend: compatBackend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *backendStorageStatic) Close() {
|
||||
}
|
||||
|
||||
func (s *backendStorageStatic) RemoveBackendsForHost(host string) {
|
||||
if oldBackends := s.backends[host]; len(oldBackends) > 0 {
|
||||
for _, backend := range oldBackends {
|
||||
log.Printf("Backend %s removed for %s", backend.id, backend.url)
|
||||
deleteBackendStats(backend)
|
||||
}
|
||||
statsBackendsCurrent.Sub(float64(len(oldBackends)))
|
||||
}
|
||||
delete(s.backends, host)
|
||||
}
|
||||
|
||||
func (s *backendStorageStatic) UpsertHost(host string, backends []*Backend) {
|
||||
for existingIndex, existingBackend := range s.backends[host] {
|
||||
found := false
|
||||
index := 0
|
||||
for _, newBackend := range backends {
|
||||
if reflect.DeepEqual(existingBackend, newBackend) { // otherwise we could manually compare the struct members here
|
||||
found = true
|
||||
backends = append(backends[:index], backends[index+1:]...)
|
||||
break
|
||||
} else if newBackend.id == existingBackend.id {
|
||||
found = true
|
||||
s.backends[host][existingIndex] = newBackend
|
||||
backends = append(backends[:index], backends[index+1:]...)
|
||||
log.Printf("Backend %s updated for %s", newBackend.id, newBackend.url)
|
||||
updateBackendStats(newBackend)
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
if !found {
|
||||
removed := s.backends[host][existingIndex]
|
||||
log.Printf("Backend %s removed for %s", removed.id, removed.url)
|
||||
s.backends[host] = append(s.backends[host][:existingIndex], s.backends[host][existingIndex+1:]...)
|
||||
deleteBackendStats(removed)
|
||||
statsBackendsCurrent.Dec()
|
||||
}
|
||||
}
|
||||
|
||||
s.backends[host] = append(s.backends[host], backends...)
|
||||
for _, added := range backends {
|
||||
log.Printf("Backend %s added for %s", added.id, added.url)
|
||||
updateBackendStats(added)
|
||||
}
|
||||
statsBackendsCurrent.Add(float64(len(backends)))
|
||||
}
|
||||
|
||||
func getConfiguredBackendIDs(backendIds string) (ids []string) {
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, id := range strings.Split(backendIds, ",") {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, id)
|
||||
seen[id] = true
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func getConfiguredHosts(backendIds string, config *goconf.ConfigFile, commonSecret string) (hosts map[string][]*Backend) {
|
||||
hosts = make(map[string][]*Backend)
|
||||
for _, id := range getConfiguredBackendIDs(backendIds) {
|
||||
u, _ := config.GetString(id, "url")
|
||||
if u == "" {
|
||||
log.Printf("Backend %s is missing or incomplete, skipping", id)
|
||||
continue
|
||||
}
|
||||
|
||||
if u[len(u)-1] != '/' {
|
||||
u += "/"
|
||||
}
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
log.Printf("Backend %s has an invalid url %s configured (%s), skipping", id, u, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(parsed.Host, ":") && hasStandardPort(parsed) {
|
||||
parsed.Host = parsed.Hostname()
|
||||
u = parsed.String()
|
||||
}
|
||||
|
||||
secret, _ := config.GetString(id, "secret")
|
||||
if secret == "" && commonSecret != "" {
|
||||
log.Printf("Backend %s has no own shared secret set, using common shared secret", id)
|
||||
secret = commonSecret
|
||||
}
|
||||
if u == "" || secret == "" {
|
||||
log.Printf("Backend %s is missing or incomplete, skipping", id)
|
||||
continue
|
||||
}
|
||||
|
||||
sessionLimit, err := config.GetInt(id, "sessionlimit")
|
||||
if err != nil || sessionLimit < 0 {
|
||||
sessionLimit = 0
|
||||
}
|
||||
if sessionLimit > 0 {
|
||||
log.Printf("Backend %s allows a maximum of %d sessions", id, sessionLimit)
|
||||
}
|
||||
|
||||
maxStreamBitrate, err := config.GetInt(id, "maxstreambitrate")
|
||||
if err != nil || maxStreamBitrate < 0 {
|
||||
maxStreamBitrate = 0
|
||||
}
|
||||
maxScreenBitrate, err := config.GetInt(id, "maxscreenbitrate")
|
||||
if err != nil || maxScreenBitrate < 0 {
|
||||
maxScreenBitrate = 0
|
||||
}
|
||||
|
||||
hosts[parsed.Host] = append(hosts[parsed.Host], &Backend{
|
||||
id: id,
|
||||
url: u,
|
||||
parsedUrl: parsed,
|
||||
secret: []byte(secret),
|
||||
|
||||
allowHttp: parsed.Scheme == "http",
|
||||
|
||||
maxStreamBitrate: maxStreamBitrate,
|
||||
maxScreenBitrate: maxScreenBitrate,
|
||||
|
||||
sessionLimit: uint64(sessionLimit),
|
||||
})
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (s *backendStorageStatic) Reload(config *goconf.ConfigFile) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.compatBackend != nil {
|
||||
log.Println("Old-style configuration active, reload is not supported")
|
||||
return
|
||||
}
|
||||
|
||||
commonSecret, _ := config.GetString("backend", "secret")
|
||||
|
||||
if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" {
|
||||
configuredHosts := getConfiguredHosts(backendIds, config, commonSecret)
|
||||
|
||||
// remove backends that are no longer configured
|
||||
for hostname := range s.backends {
|
||||
if _, ok := configuredHosts[hostname]; !ok {
|
||||
s.RemoveBackendsForHost(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// rewrite backends adding newly configured ones and rewriting existing ones
|
||||
for hostname, configuredBackends := range configuredHosts {
|
||||
s.UpsertHost(hostname, configuredBackends)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *backendStorageStatic) GetCompatBackend() *Backend {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.compatBackend
|
||||
}
|
||||
|
||||
func (s *backendStorageStatic) GetBackend(u *url.URL) *Backend {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if _, found := s.backends[u.Host]; !found {
|
||||
if s.allowAll {
|
||||
return s.compatBackend
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.getBackendLocked(u)
|
||||
}
|
||||
|
|
@ -19,14 +19,14 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -36,6 +36,12 @@ import (
|
|||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mailru/easyjson"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/geoip"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -52,136 +58,112 @@ const (
|
|||
maxMessageSize = 64 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
noCountry = "no-country"
|
||||
|
||||
loopback = "loopback"
|
||||
|
||||
unknownCountry = "unknown-country"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterClientStats()
|
||||
}
|
||||
|
||||
func IsValidCountry(country string) bool {
|
||||
switch country {
|
||||
case "":
|
||||
fallthrough
|
||||
case noCountry:
|
||||
fallthrough
|
||||
case loopback:
|
||||
fallthrough
|
||||
case unknownCountry:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
InvalidFormat = NewError("invalid_format", "Invalid data format.")
|
||||
InvalidFormat = api.NewError("invalid_format", "Invalid data format.")
|
||||
|
||||
bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
bufferPool pool.BufferPool
|
||||
)
|
||||
|
||||
type WritableClientMessage interface {
|
||||
json.Marshaler
|
||||
|
||||
CloseAfterSend(session Session) bool
|
||||
CloseAfterSend(session api.RoomAware) bool
|
||||
}
|
||||
|
||||
type HandlerClient interface {
|
||||
Context() context.Context
|
||||
RemoteAddr() string
|
||||
Country() string
|
||||
Country() geoip.Country
|
||||
UserAgent() string
|
||||
IsConnected() bool
|
||||
IsAuthenticated() bool
|
||||
|
||||
GetSession() Session
|
||||
SetSession(session Session)
|
||||
|
||||
SendError(e *Error) bool
|
||||
SendByeResponse(message *ClientMessage) bool
|
||||
SendByeResponseWithReason(message *ClientMessage, reason string) bool
|
||||
SendError(e *api.Error) bool
|
||||
SendByeResponse(message *api.ClientMessage) bool
|
||||
SendByeResponseWithReason(message *api.ClientMessage, reason string) bool
|
||||
SendMessage(message WritableClientMessage) bool
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
type ClientHandler interface {
|
||||
OnClosed(HandlerClient)
|
||||
OnMessageReceived(HandlerClient, []byte)
|
||||
OnRTTReceived(HandlerClient, time.Duration)
|
||||
type Handler interface {
|
||||
GetSessionId() api.PublicSessionId
|
||||
|
||||
OnClosed()
|
||||
OnMessageReceived([]byte)
|
||||
OnRTTReceived(time.Duration)
|
||||
}
|
||||
|
||||
type ClientGeoIpHandler interface {
|
||||
OnLookupCountry(HandlerClient) string
|
||||
type GeoIpHandler interface {
|
||||
OnLookupCountry(addr string) geoip.Country
|
||||
}
|
||||
|
||||
type InRoomHandler interface {
|
||||
IsInRoom(string) bool
|
||||
}
|
||||
|
||||
type SessionCloserHandler interface {
|
||||
CloseSession()
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
logger log.Logger
|
||||
ctx context.Context
|
||||
// +checklocks:mu
|
||||
conn *websocket.Conn
|
||||
addr string
|
||||
agent string
|
||||
closed atomic.Int32
|
||||
country *string
|
||||
country *geoip.Country
|
||||
logRTT bool
|
||||
|
||||
handlerMu sync.RWMutex
|
||||
handler ClientHandler
|
||||
// +checklocks:handlerMu
|
||||
handler Handler
|
||||
|
||||
session atomic.Pointer[Session]
|
||||
sessionId atomic.Pointer[string]
|
||||
sessionId atomic.Pointer[api.PublicSessionId]
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
closer *Closer
|
||||
closer *internal.Closer
|
||||
closeOnce sync.Once
|
||||
messagesDone chan struct{}
|
||||
messageChan chan *bytes.Buffer
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conn *websocket.Conn, remoteAddress string, agent string, handler ClientHandler) (*Client, error) {
|
||||
remoteAddress = strings.TrimSpace(remoteAddress)
|
||||
if remoteAddress == "" {
|
||||
remoteAddress = "unknown remote address"
|
||||
}
|
||||
agent = strings.TrimSpace(agent)
|
||||
if agent == "" {
|
||||
agent = "unknown user agent"
|
||||
}
|
||||
func (c *Client) SetConn(ctx context.Context, conn *websocket.Conn, remoteAddress string, agent string, logRTT bool, handler Handler) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
client := &Client{
|
||||
agent: agent,
|
||||
logRTT: true,
|
||||
}
|
||||
client.SetConn(ctx, conn, remoteAddress, handler)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetConn(ctx context.Context, conn *websocket.Conn, remoteAddress string, handler ClientHandler) {
|
||||
c.logger = log.LoggerFromContext(ctx)
|
||||
c.ctx = ctx
|
||||
c.conn = conn
|
||||
c.addr = remoteAddress
|
||||
c.agent = agent
|
||||
c.logRTT = logRTT
|
||||
c.SetHandler(handler)
|
||||
c.closer = NewCloser()
|
||||
c.closer = internal.NewCloser()
|
||||
c.messageChan = make(chan *bytes.Buffer, 16)
|
||||
c.messagesDone = make(chan struct{})
|
||||
}
|
||||
|
||||
func (c *Client) SetHandler(handler ClientHandler) {
|
||||
func (c *Client) GetConn() *websocket.Conn {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.conn
|
||||
}
|
||||
|
||||
func (c *Client) SetHandler(handler Handler) {
|
||||
c.handlerMu.Lock()
|
||||
defer c.handlerMu.Unlock()
|
||||
c.handler = handler
|
||||
}
|
||||
|
||||
func (c *Client) getHandler() ClientHandler {
|
||||
func (c *Client) getHandler() Handler {
|
||||
c.handlerMu.RLock()
|
||||
defer c.handlerMu.RUnlock()
|
||||
return c.handler
|
||||
|
|
@ -195,40 +177,19 @@ func (c *Client) IsConnected() bool {
|
|||
return c.closed.Load() == 0
|
||||
}
|
||||
|
||||
func (c *Client) IsAuthenticated() bool {
|
||||
return c.GetSession() != nil
|
||||
}
|
||||
|
||||
func (c *Client) GetSession() Session {
|
||||
session := c.session.Load()
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return *session
|
||||
}
|
||||
|
||||
func (c *Client) SetSession(session Session) {
|
||||
if session == nil {
|
||||
c.session.Store(nil)
|
||||
} else {
|
||||
c.session.Store(&session)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetSessionId(sessionId string) {
|
||||
func (c *Client) SetSessionId(sessionId api.PublicSessionId) {
|
||||
c.sessionId.Store(&sessionId)
|
||||
}
|
||||
|
||||
func (c *Client) GetSessionId() string {
|
||||
func (c *Client) GetSessionId() api.PublicSessionId {
|
||||
sessionId := c.sessionId.Load()
|
||||
if sessionId == nil {
|
||||
session := c.GetSession()
|
||||
if session == nil {
|
||||
sessionId := c.getHandler().GetSessionId()
|
||||
if sessionId == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return session.PublicId()
|
||||
return sessionId
|
||||
}
|
||||
|
||||
return *sessionId
|
||||
|
|
@ -242,13 +203,13 @@ func (c *Client) UserAgent() string {
|
|||
return c.agent
|
||||
}
|
||||
|
||||
func (c *Client) Country() string {
|
||||
func (c *Client) Country() geoip.Country {
|
||||
if c.country == nil {
|
||||
var country string
|
||||
if handler, ok := c.getHandler().(ClientGeoIpHandler); ok {
|
||||
country = handler.OnLookupCountry(c)
|
||||
var country geoip.Country
|
||||
if handler, ok := c.getHandler().(GeoIpHandler); ok {
|
||||
country = handler.OnLookupCountry(c.addr)
|
||||
} else {
|
||||
country = unknownCountry
|
||||
country = geoip.UnknownCountry
|
||||
}
|
||||
c.country = &country
|
||||
}
|
||||
|
|
@ -256,6 +217,14 @@ func (c *Client) Country() string {
|
|||
return *c.country
|
||||
}
|
||||
|
||||
func (c *Client) IsInRoom(id string) bool {
|
||||
if handler, ok := c.getHandler().(InRoomHandler); ok {
|
||||
return handler.IsInRoom(id)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
if c.closed.Load() >= 2 {
|
||||
// Prevent reentrant call in case this was the second closing
|
||||
|
|
@ -271,7 +240,8 @@ func (c *Client) Close() {
|
|||
|
||||
func (c *Client) doClose() {
|
||||
closed := c.closed.Add(1)
|
||||
if closed == 1 {
|
||||
switch closed {
|
||||
case 1:
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn != nil {
|
||||
|
|
@ -279,30 +249,29 @@ func (c *Client) doClose() {
|
|||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
} else if closed == 2 {
|
||||
case 2:
|
||||
// Both the read pump and message processing must be finished before closing.
|
||||
c.closer.Close()
|
||||
<-c.messagesDone
|
||||
|
||||
c.getHandler().OnClosed(c)
|
||||
c.SetSession(nil)
|
||||
c.getHandler().OnClosed()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SendError(e *Error) bool {
|
||||
message := &ServerMessage{
|
||||
func (c *Client) SendError(e *api.Error) bool {
|
||||
message := &api.ServerMessage{
|
||||
Type: "error",
|
||||
Error: e,
|
||||
}
|
||||
return c.SendMessage(message)
|
||||
}
|
||||
|
||||
func (c *Client) SendByeResponse(message *ClientMessage) bool {
|
||||
func (c *Client) SendByeResponse(message *api.ClientMessage) bool {
|
||||
return c.SendByeResponseWithReason(message, "")
|
||||
}
|
||||
|
||||
func (c *Client) SendByeResponseWithReason(message *ClientMessage, reason string) bool {
|
||||
response := &ServerMessage{
|
||||
func (c *Client) SendByeResponseWithReason(message *api.ClientMessage, reason string) bool {
|
||||
response := &api.ServerMessage{
|
||||
Type: "bye",
|
||||
}
|
||||
if message != nil {
|
||||
|
|
@ -310,7 +279,7 @@ func (c *Client) SendByeResponseWithReason(message *ClientMessage, reason string
|
|||
}
|
||||
if reason != "" {
|
||||
if response.Bye == nil {
|
||||
response.Bye = &ByeServerMessage{}
|
||||
response.Bye = &api.ByeServerMessage{}
|
||||
}
|
||||
response.Bye.Reason = reason
|
||||
}
|
||||
|
|
@ -334,7 +303,7 @@ func (c *Client) ReadPump() {
|
|||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
if conn == nil {
|
||||
log.Printf("Connection from %s closed while starting readPump", addr)
|
||||
c.logger.Printf("Connection from %s closed while starting readPump", addr)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -345,17 +314,19 @@ func (c *Client) ReadPump() {
|
|||
if msg == "" {
|
||||
return nil
|
||||
}
|
||||
statsClientBytesTotal.WithLabelValues("incoming").Add(float64(len(msg)))
|
||||
if ts, err := strconv.ParseInt(msg, 10, 64); err == nil {
|
||||
rtt := now.Sub(time.Unix(0, ts))
|
||||
if c.logRTT {
|
||||
rtt_ms := rtt.Nanoseconds() / time.Millisecond.Nanoseconds()
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Client %s has RTT of %d ms (%s)", sessionId, rtt_ms, rtt)
|
||||
c.logger.Printf("Client %s has RTT of %d ms (%s)", sessionId, rtt_ms, rtt)
|
||||
} else {
|
||||
log.Printf("Client from %s has RTT of %d ms (%s)", addr, rtt_ms, rtt)
|
||||
c.logger.Printf("Client from %s has RTT of %d ms (%s)", addr, rtt_ms, rtt)
|
||||
}
|
||||
}
|
||||
c.getHandler().OnRTTReceived(c, rtt)
|
||||
statsClientRTT.Observe(float64(rtt.Milliseconds()))
|
||||
c.getHandler().OnRTTReceived(rtt)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
@ -372,9 +343,9 @@ func (c *Client) ReadPump() {
|
|||
websocket.CloseGoingAway,
|
||||
websocket.CloseNoStatusReceived) {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Error reading from client %s: %v", sessionId, err)
|
||||
c.logger.Printf("Error reading from client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Error reading from %s: %v", addr, err)
|
||||
c.logger.Printf("Error reading from %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
@ -382,22 +353,20 @@ func (c *Client) ReadPump() {
|
|||
|
||||
if messageType != websocket.TextMessage {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Unsupported message type %v from client %s", messageType, sessionId)
|
||||
c.logger.Printf("Unsupported message type %v from client %s", messageType, sessionId)
|
||||
} else {
|
||||
log.Printf("Unsupported message type %v from %s", messageType, addr)
|
||||
c.logger.Printf("Unsupported message type %v from %s", messageType, addr)
|
||||
}
|
||||
c.SendError(InvalidFormat)
|
||||
continue
|
||||
}
|
||||
|
||||
decodeBuffer := bufferPool.Get().(*bytes.Buffer)
|
||||
decodeBuffer.Reset()
|
||||
if _, err := decodeBuffer.ReadFrom(reader); err != nil {
|
||||
bufferPool.Put(decodeBuffer)
|
||||
decodeBuffer, err := bufferPool.ReadAll(reader)
|
||||
if err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Error reading message from client %s: %v", sessionId, err)
|
||||
c.logger.Printf("Error reading message from client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Error reading message from %s: %v", addr, err)
|
||||
c.logger.Printf("Error reading message from %s: %v", addr, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -408,6 +377,8 @@ func (c *Client) ReadPump() {
|
|||
break
|
||||
}
|
||||
|
||||
statsClientBytesTotal.WithLabelValues("incoming").Add(float64(decodeBuffer.Len()))
|
||||
statsClientMessagesTotal.WithLabelValues("incoming").Inc()
|
||||
c.messageChan <- decodeBuffer
|
||||
}
|
||||
}
|
||||
|
|
@ -419,7 +390,7 @@ func (c *Client) processMessages() {
|
|||
break
|
||||
}
|
||||
|
||||
c.getHandler().OnMessageReceived(c, buffer.Bytes())
|
||||
c.getHandler().OnMessageReceived(buffer.Bytes())
|
||||
bufferPool.Put(buffer)
|
||||
}
|
||||
|
||||
|
|
@ -427,16 +398,34 @@ func (c *Client) processMessages() {
|
|||
c.doClose()
|
||||
}
|
||||
|
||||
type counterWriter struct {
|
||||
w io.Writer
|
||||
counter *int
|
||||
}
|
||||
|
||||
func (w *counterWriter) Write(p []byte) (int, error) {
|
||||
written, err := w.w.Write(p)
|
||||
if written > 0 {
|
||||
*w.counter += written
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *Client) writeInternal(message json.Marshaler) bool {
|
||||
var closeData []byte
|
||||
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
writer, err := c.conn.NextWriter(websocket.TextMessage)
|
||||
var written int
|
||||
if err == nil {
|
||||
if m, ok := (interface{}(message)).(easyjson.Marshaler); ok {
|
||||
_, err = easyjson.MarshalToWriter(m, writer)
|
||||
if m, ok := (any(message)).(easyjson.Marshaler); ok {
|
||||
written, err = easyjson.MarshalToWriter(m, writer)
|
||||
} else {
|
||||
err = json.NewEncoder(writer).Encode(message)
|
||||
err = json.NewEncoder(&counterWriter{
|
||||
w: writer,
|
||||
counter: &written,
|
||||
}).Encode(message)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
|
|
@ -449,49 +438,25 @@ func (c *Client) writeInternal(message json.Marshaler) bool {
|
|||
}
|
||||
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Could not send message %+v to client %s: %v", message, sessionId, err)
|
||||
c.logger.Printf("Could not send message %+v to client %s: %v", message, sessionId, err)
|
||||
} else {
|
||||
log.Printf("Could not send message %+v to %s: %v", message, c.RemoteAddr(), err)
|
||||
c.logger.Printf("Could not send message %+v to %s: %v", message, c.RemoteAddr(), err)
|
||||
}
|
||||
closeData = websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "")
|
||||
goto close
|
||||
}
|
||||
|
||||
statsClientBytesTotal.WithLabelValues("outgoing").Add(float64(written))
|
||||
statsClientMessagesTotal.WithLabelValues("outgoing").Inc()
|
||||
return true
|
||||
|
||||
close:
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Could not send close message to client %s: %v", sessionId, err)
|
||||
c.logger.Printf("Could not send close message to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) writeError(e error) bool { // nolint
|
||||
message := &ServerMessage{
|
||||
Type: "error",
|
||||
Error: NewError("internal_error", e.Error()),
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !c.writeMessageLocked(message) {
|
||||
return false
|
||||
}
|
||||
|
||||
closeData := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, e.Error())
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Could not send close message to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err)
|
||||
c.logger.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
@ -507,19 +472,19 @@ func (c *Client) writeMessage(message WritableClientMessage) bool {
|
|||
return c.writeMessageLocked(message)
|
||||
}
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *Client) writeMessageLocked(message WritableClientMessage) bool {
|
||||
if !c.writeInternal(message) {
|
||||
return false
|
||||
}
|
||||
|
||||
session := c.GetSession()
|
||||
if message.CloseAfterSend(session) {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
c.conn.WriteMessage(websocket.CloseMessage, []byte{}) // nolint
|
||||
if session != nil {
|
||||
go session.Close()
|
||||
}
|
||||
go c.Close()
|
||||
if message.CloseAfterSend(c) {
|
||||
go func() {
|
||||
if sc, ok := c.getHandler().(SessionCloserHandler); ok {
|
||||
sc.CloseSession()
|
||||
}
|
||||
c.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
@ -537,13 +502,14 @@ func (c *Client) sendPing() bool {
|
|||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Could not send ping to client %s: %v", sessionId, err)
|
||||
c.logger.Printf("Could not send ping to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Could not send ping to %s: %v", c.RemoteAddr(), err)
|
||||
c.logger.Printf("Could not send ping to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
statsClientBytesTotal.WithLabelValues("outgoing").Add(float64(len(msg)))
|
||||
return true
|
||||
}
|
||||
|
||||
339
client/client_test.go
Normal file
339
client/client_test.go
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/geoip"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
)
|
||||
|
||||
func TestCounterWriter(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
var b bytes.Buffer
|
||||
var written int
|
||||
w := &counterWriter{
|
||||
w: &b,
|
||||
counter: &written,
|
||||
}
|
||||
if count, err := w.Write(nil); assert.NoError(err) && assert.Equal(0, count) {
|
||||
assert.Equal(0, written)
|
||||
}
|
||||
if count, err := w.Write([]byte("foo")); assert.NoError(err) && assert.Equal(3, count) {
|
||||
assert.Equal(3, written)
|
||||
}
|
||||
}
|
||||
|
||||
type serverClient struct {
|
||||
Client
|
||||
|
||||
t *testing.T
|
||||
handler *testHandler
|
||||
|
||||
id string
|
||||
received atomic.Uint32
|
||||
sessionClosed atomic.Bool
|
||||
}
|
||||
|
||||
func newTestClient(h *testHandler, r *http.Request, conn *websocket.Conn, id uint64) *serverClient {
|
||||
result := &serverClient{
|
||||
t: h.t,
|
||||
handler: h,
|
||||
id: fmt.Sprintf("session-%d", id),
|
||||
}
|
||||
|
||||
addr := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
logger := logtest.NewLoggerForTest(h.t)
|
||||
ctx := log.NewLoggerContext(r.Context(), logger)
|
||||
result.SetConn(ctx, conn, addr, r.Header.Get("User-Agent"), false, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *serverClient) WaitReceived(ctx context.Context, count uint32) error {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
} else if c.received.Load() >= count {
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverClient) GetSessionId() api.PublicSessionId {
|
||||
return api.PublicSessionId(c.id)
|
||||
}
|
||||
|
||||
func (c *serverClient) OnClosed() {
|
||||
c.Close()
|
||||
c.handler.removeClient(c)
|
||||
}
|
||||
|
||||
func (c *serverClient) OnMessageReceived(message []byte) {
|
||||
switch c.received.Add(1) {
|
||||
case 1:
|
||||
var s string
|
||||
if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) {
|
||||
assert.Equal(c.t, "Hello world!", s)
|
||||
c.sendPing()
|
||||
assert.EqualValues(c.t, "DE", c.Country())
|
||||
assert.False(c.t, c.Client.IsInRoom("room-id"))
|
||||
c.SendMessage(&api.ServerMessage{
|
||||
Type: "welcome",
|
||||
Welcome: &api.WelcomeServerMessage{
|
||||
Version: "1.0",
|
||||
},
|
||||
})
|
||||
}
|
||||
case 2:
|
||||
var s string
|
||||
if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) {
|
||||
assert.Equal(c.t, "Send error", s)
|
||||
c.SendError(api.NewError("test_error", "This is a test error."))
|
||||
}
|
||||
case 3:
|
||||
var s string
|
||||
if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) {
|
||||
assert.Equal(c.t, "Send bye", s)
|
||||
c.SendByeResponseWithReason(nil, "Go away!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverClient) OnRTTReceived(rtt time.Duration) {
|
||||
|
||||
}
|
||||
|
||||
func (c *serverClient) OnLookupCountry(addr string) geoip.Country {
|
||||
return "DE"
|
||||
}
|
||||
|
||||
func (c *serverClient) IsInRoom(roomId string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *serverClient) CloseSession() {
|
||||
if c.sessionClosed.Swap(true) {
|
||||
assert.Fail(c.t, "session closed more than once")
|
||||
}
|
||||
}
|
||||
|
||||
type testHandler struct {
|
||||
mu sync.Mutex
|
||||
|
||||
t *testing.T
|
||||
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
id atomic.Uint64
|
||||
// +checklocks:mu
|
||||
activeClients map[string]*serverClient
|
||||
// +checklocks:mu
|
||||
allClients []*serverClient
|
||||
}
|
||||
|
||||
func newTestHandler(t *testing.T) *testHandler {
|
||||
return &testHandler{
|
||||
t: t,
|
||||
activeClients: make(map[string]*serverClient),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *testHandler) addClient(client *serverClient) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.activeClients[client.id] = client
|
||||
h.allClients = append(h.allClients, client)
|
||||
}
|
||||
|
||||
func (h *testHandler) removeClient(client *serverClient) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
delete(h.activeClients, client.id)
|
||||
}
|
||||
|
||||
func (h *testHandler) getClients() []*serverClient {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return slices.Clone(h.allClients)
|
||||
}
|
||||
|
||||
func (h *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := h.upgrader.Upgrade(w, r, nil)
|
||||
if !assert.NoError(h.t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
id := h.id.Add(1)
|
||||
client := newTestClient(h, r, conn, id)
|
||||
h.addClient(client)
|
||||
|
||||
closed := make(chan struct{})
|
||||
context.AfterFunc(client.Context(), func() {
|
||||
close(closed)
|
||||
})
|
||||
|
||||
go client.WritePump()
|
||||
client.ReadPump()
|
||||
<-closed
|
||||
}
|
||||
|
||||
type localClient struct {
|
||||
t *testing.T
|
||||
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func newLocalClient(t *testing.T, url string) *localClient {
|
||||
t.Helper()
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(t.Context(), url, nil)
|
||||
require.NoError(t, err)
|
||||
return &localClient{
|
||||
t: t,
|
||||
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) Close() error {
|
||||
err := c.conn.Close()
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *localClient) WriteJSON(v any) error {
|
||||
return c.conn.WriteJSON(v)
|
||||
}
|
||||
|
||||
func (c *localClient) Write(v []byte) error {
|
||||
return c.conn.WriteMessage(websocket.BinaryMessage, v)
|
||||
}
|
||||
|
||||
func (c *localClient) ReadJSON(v any) error {
|
||||
return c.conn.ReadJSON(v)
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
serverHandler := newTestHandler(t)
|
||||
|
||||
server := httptest.NewServer(serverHandler)
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
|
||||
client := newLocalClient(t, strings.ReplaceAll(server.URL, "http://", "ws://"))
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(client.Close())
|
||||
})
|
||||
|
||||
var msg api.ServerMessage
|
||||
|
||||
require.NoError(client.WriteJSON("Hello world!"))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("welcome", msg.Type) &&
|
||||
assert.NotNil(msg.Welcome) {
|
||||
assert.Equal("1.0", msg.Welcome.Version)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.False(clients[0].sessionClosed.Load())
|
||||
assert.EqualValues(1, clients[0].received.Load())
|
||||
}
|
||||
|
||||
require.NoError(client.Write([]byte("Hello world!")))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("error", msg.Type) &&
|
||||
assert.NotNil(msg.Error) {
|
||||
assert.Equal("invalid_format", msg.Error.Code)
|
||||
assert.Equal("Invalid data format.", msg.Error.Message)
|
||||
}
|
||||
|
||||
require.NoError(client.WriteJSON("Send error"))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("error", msg.Type) &&
|
||||
assert.NotNil(msg.Error) {
|
||||
assert.Equal("test_error", msg.Error.Code)
|
||||
assert.Equal("This is a test error.", msg.Error.Message)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.False(clients[0].sessionClosed.Load())
|
||||
assert.EqualValues(2, clients[0].received.Load())
|
||||
}
|
||||
|
||||
require.NoError(client.WriteJSON("Send bye"))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("bye", msg.Type) &&
|
||||
assert.NotNil(msg.Bye) {
|
||||
assert.Equal("Go away!", msg.Bye.Reason)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.EqualValues(3, clients[0].received.Load())
|
||||
}
|
||||
|
||||
// Sending a "bye" will close the connection.
|
||||
var we *websocket.CloseError
|
||||
if err := client.ReadJSON(&msg); assert.ErrorAs(err, &we) {
|
||||
assert.Equal(websocket.CloseNormalClosure, we.Code)
|
||||
assert.Empty(we.Text)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.True(clients[0].sessionClosed.Load())
|
||||
assert.EqualValues(3, clients[0].received.Load())
|
||||
}
|
||||
}
|
||||
93
client/ip.go
Normal file
93
client/ip.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/container"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultTrustedProxies = container.DefaultPrivateIPs()
|
||||
)
|
||||
|
||||
func GetRealUserIP(r *http.Request, trusted *container.IPList) string {
|
||||
addr := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
ip := net.ParseIP(addr)
|
||||
if len(ip) == 0 {
|
||||
return addr
|
||||
}
|
||||
|
||||
// Don't check any headers if the server can be reached by untrusted clients directly.
|
||||
if trusted == nil || !trusted.Contains(ip) {
|
||||
return addr
|
||||
}
|
||||
|
||||
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
|
||||
if ip := net.ParseIP(realIP); len(ip) > 0 {
|
||||
return realIP
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address
|
||||
forwarded := strings.Split(strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",")
|
||||
if len(forwarded) > 0 {
|
||||
slices.Reverse(forwarded)
|
||||
var lastTrusted string
|
||||
for _, hop := range forwarded {
|
||||
hop = strings.TrimSpace(hop)
|
||||
// Make sure to remove any port.
|
||||
if host, _, err := net.SplitHostPort(hop); err == nil {
|
||||
hop = host
|
||||
}
|
||||
|
||||
ip := net.ParseIP(hop)
|
||||
if len(ip) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if trusted.Contains(ip) {
|
||||
lastTrusted = hop
|
||||
continue
|
||||
}
|
||||
|
||||
return hop
|
||||
}
|
||||
|
||||
// If all entries in the "X-Forwarded-For" list are trusted, the left-most
|
||||
// will be the client IP. This can happen if a subnet is trusted and the
|
||||
// client also has an IP from this subnet.
|
||||
if lastTrusted != "" {
|
||||
return lastTrusted
|
||||
}
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
278
client/ip_test.go
Normal file
278
client/ip_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/container"
|
||||
)
|
||||
|
||||
func TestGetRealUserIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
testcases := []struct {
|
||||
expected string
|
||||
headers http.Header
|
||||
trusted string
|
||||
addr string
|
||||
}{
|
||||
{
|
||||
"192.168.1.2",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"invalid-ip",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"invalid-ip",
|
||||
},
|
||||
{
|
||||
"invalid-ip",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"invalid-ip:12345",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14:1234, 192.168.30.32:2345"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"},
|
||||
},
|
||||
"2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"},
|
||||
},
|
||||
"2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::1"},
|
||||
},
|
||||
"192.168.0.0/16, 2001:db8::/48",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16, 2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::2"},
|
||||
},
|
||||
"2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
// "X-Real-IP" has preference before "X-Forwarded-For"
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"},
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
// Multiple "X-Forwarded-For" headers are merged.
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14", "192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "11.12.13.14", "192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "2.3.4.5", "11.12.13.14", "192.168.31.32", "192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
// Headers are ignored if coming from untrusted clients.
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"11.12.13.14"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
// X-Forwarded-For is filtered for trusted proxies.
|
||||
{
|
||||
"1.2.3.4",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"1.2.3.4",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4, 192.168.2.3"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
// Invalid IPs are ignored.
|
||||
{
|
||||
"192.168.1.2",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"},
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"},
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32, proxy1"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"192.168.1.2",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"192.168.2.3",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip, 192.168.2.3"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
trustedProxies, err := container.ParseIPList(tc.trusted)
|
||||
if !assert.NoError(t, err, "invalid trusted proxies in %+v", tc) {
|
||||
continue
|
||||
}
|
||||
request := &http.Request{
|
||||
RemoteAddr: tc.addr,
|
||||
Header: tc.headers,
|
||||
}
|
||||
assert.Equal(t, tc.expected, GetRealUserIP(request, trustedProxies), "failed for %+v", tc)
|
||||
}
|
||||
}
|
||||
60
client/stats_prometheus.go
Normal file
60
client/stats_prometheus.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2021 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
statsClientRTT = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "client",
|
||||
Name: "rtt",
|
||||
Help: "The roundtrip time of WebSocket ping messages in milliseconds",
|
||||
Buckets: prometheus.ExponentialBucketsRange(1, 30000, 50),
|
||||
})
|
||||
statsClientBytesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "client",
|
||||
Name: "bytes_total",
|
||||
Help: "The total number of bytes sent to or received by clients",
|
||||
}, []string{"direction"})
|
||||
statsClientMessagesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "client",
|
||||
Name: "messages_total",
|
||||
Help: "The total number of messages sent to or received by clients",
|
||||
}, []string{"direction"})
|
||||
|
||||
clientStats = []prometheus.Collector{
|
||||
statsClientRTT,
|
||||
statsClientBytesTotal,
|
||||
statsClientMessagesTotal,
|
||||
}
|
||||
)
|
||||
|
||||
func RegisterClientStats() {
|
||||
metrics.RegisterAll(clientStats...)
|
||||
}
|
||||
1500
clientsession.go
1500
clientsession.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,267 +0,0 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2019 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
equalStrings = map[bool]string{
|
||||
true: "equal",
|
||||
false: "not equal",
|
||||
}
|
||||
)
|
||||
|
||||
type EqualTestData struct {
|
||||
a map[Permission]bool
|
||||
b map[Permission]bool
|
||||
equal bool
|
||||
}
|
||||
|
||||
func Test_permissionsEqual(t *testing.T) {
|
||||
tests := []EqualTestData{
|
||||
{
|
||||
a: nil,
|
||||
b: nil,
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
a: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
},
|
||||
b: nil,
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
a: nil,
|
||||
b: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
a: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
},
|
||||
b: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
},
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
a: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
PERMISSION_MAY_PUBLISH_SCREEN: true,
|
||||
},
|
||||
b: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
a: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
},
|
||||
b: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
PERMISSION_MAY_PUBLISH_SCREEN: true,
|
||||
},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
a: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
PERMISSION_MAY_PUBLISH_SCREEN: true,
|
||||
},
|
||||
b: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
PERMISSION_MAY_PUBLISH_SCREEN: true,
|
||||
},
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
a: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
PERMISSION_MAY_PUBLISH_SCREEN: true,
|
||||
},
|
||||
b: map[Permission]bool{
|
||||
PERMISSION_MAY_PUBLISH_MEDIA: true,
|
||||
PERMISSION_MAY_PUBLISH_SCREEN: false,
|
||||
},
|
||||
equal: false,
|
||||
},
|
||||
}
|
||||
for idx, test := range tests {
|
||||
test := test
|
||||
t.Run(strconv.Itoa(idx), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
equal := permissionsEqual(test.a, test.b)
|
||||
assert.Equal(t, test.equal, equal, "Expected %+v to be %s to %+v but was %s", test.a, equalStrings[test.equal], test.b, equalStrings[equal])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBandwidth_Client(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
hub, _, _, server := CreateHubForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
mcu, err := NewTestMCU()
|
||||
require.NoError(err)
|
||||
require.NoError(mcu.Start(ctx))
|
||||
defer mcu.Stop()
|
||||
|
||||
hub.SetMcu(mcu)
|
||||
|
||||
client := NewTestClient(t, server, hub)
|
||||
defer client.CloseWithBye()
|
||||
|
||||
require.NoError(client.SendHello(testDefaultUserId))
|
||||
|
||||
hello, err := client.RunUntilHello(ctx)
|
||||
require.NoError(err)
|
||||
|
||||
// Join room by id.
|
||||
roomId := "test-room"
|
||||
roomMsg, err := client.JoinRoom(ctx, roomId)
|
||||
require.NoError(err)
|
||||
require.Equal(roomId, roomMsg.Room.RoomId)
|
||||
|
||||
// We will receive a "joined" event.
|
||||
assert.NoError(client.RunUntilJoined(ctx, hello.Hello))
|
||||
|
||||
// Client may not send an offer with audio and video.
|
||||
bitrate := 10000
|
||||
require.NoError(client.SendMessage(MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
SessionId: hello.Hello.SessionId,
|
||||
}, MessageClientMessageData{
|
||||
Type: "offer",
|
||||
Sid: "54321",
|
||||
RoomType: "video",
|
||||
Bitrate: bitrate,
|
||||
Payload: map[string]interface{}{
|
||||
"sdp": MockSdpOfferAudioAndVideo,
|
||||
},
|
||||
}))
|
||||
|
||||
require.NoError(client.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo))
|
||||
|
||||
pub := mcu.GetPublisher(hello.Hello.SessionId)
|
||||
require.NotNil(pub)
|
||||
assert.Equal(bitrate, pub.settings.Bitrate)
|
||||
}
|
||||
|
||||
func TestBandwidth_Backend(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
hub, _, _, server := CreateHubWithMultipleBackendsForTest(t)
|
||||
|
||||
u, err := url.Parse(server.URL + "/one")
|
||||
require.NoError(t, err)
|
||||
backend := hub.backend.GetBackend(u)
|
||||
require.NotNil(t, backend, "Could not get backend")
|
||||
|
||||
backend.maxScreenBitrate = 1000
|
||||
backend.maxStreamBitrate = 2000
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
mcu, err := NewTestMCU()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mcu.Start(ctx))
|
||||
defer mcu.Stop()
|
||||
|
||||
hub.SetMcu(mcu)
|
||||
|
||||
streamTypes := []StreamType{
|
||||
StreamTypeVideo,
|
||||
StreamTypeScreen,
|
||||
}
|
||||
|
||||
for _, streamType := range streamTypes {
|
||||
t.Run(string(streamType), func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
client := NewTestClient(t, server, hub)
|
||||
defer client.CloseWithBye()
|
||||
|
||||
params := TestBackendClientAuthParams{
|
||||
UserId: testDefaultUserId,
|
||||
}
|
||||
require.NoError(client.SendHelloParams(server.URL+"/one", HelloVersionV1, "client", nil, params))
|
||||
|
||||
hello, err := client.RunUntilHello(ctx)
|
||||
require.NoError(err)
|
||||
|
||||
// Join room by id.
|
||||
roomId := "test-room"
|
||||
roomMsg, err := client.JoinRoom(ctx, roomId)
|
||||
require.NoError(err)
|
||||
require.Equal(roomId, roomMsg.Room.RoomId)
|
||||
|
||||
// We will receive a "joined" event.
|
||||
require.NoError(client.RunUntilJoined(ctx, hello.Hello))
|
||||
|
||||
// Client may not send an offer with audio and video.
|
||||
bitrate := 10000
|
||||
require.NoError(client.SendMessage(MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
SessionId: hello.Hello.SessionId,
|
||||
}, MessageClientMessageData{
|
||||
Type: "offer",
|
||||
Sid: "54321",
|
||||
RoomType: string(streamType),
|
||||
Bitrate: bitrate,
|
||||
Payload: map[string]interface{}{
|
||||
"sdp": MockSdpOfferAudioAndVideo,
|
||||
},
|
||||
}))
|
||||
|
||||
require.NoError(client.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo))
|
||||
|
||||
pub := mcu.GetPublisher(hello.Hello.SessionId)
|
||||
require.NotNil(pub, "Could not find publisher")
|
||||
|
||||
var expectBitrate int
|
||||
if streamType == StreamTypeVideo {
|
||||
expectBitrate = backend.maxStreamBitrate
|
||||
} else {
|
||||
expectBitrate = backend.maxScreenBitrate
|
||||
}
|
||||
assert.Equal(expectBitrate, pub.settings.Bitrate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
|
@ -44,14 +44,27 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/mailru/easyjson/jlexer"
|
||||
"github.com/mailru/easyjson/jwriter"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/config"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/talk"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "unreleased"
|
||||
|
||||
showVersion = flag.Bool("version", false, "show version and quit")
|
||||
|
||||
addr = flag.String("addr", "localhost:28080", "http service address")
|
||||
|
||||
config = flag.String("config", "server.conf", "config file to use")
|
||||
configFlag = flag.String("config", "server.conf", "config file to use")
|
||||
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to file")
|
||||
|
||||
maxClients = flag.Int("maxClients", 100, "number of client connections")
|
||||
|
||||
|
|
@ -75,47 +88,47 @@ const (
|
|||
maxMessageSize = 64 * 1024
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
numRecvMessages atomic.Uint64
|
||||
numSentMessages atomic.Uint64
|
||||
resetRecvMessages uint64
|
||||
resetSentMessages uint64
|
||||
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func (s *Stats) reset(start time.Time) {
|
||||
s.resetRecvMessages = s.numRecvMessages.Load()
|
||||
s.resetSentMessages = s.numSentMessages.Load()
|
||||
s.start = start
|
||||
}
|
||||
|
||||
func (s *Stats) Log() {
|
||||
now := time.Now()
|
||||
duration := now.Sub(s.start)
|
||||
perSec := uint64(duration / time.Second)
|
||||
if perSec == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
totalSentMessages := s.numSentMessages.Load()
|
||||
sentMessages := totalSentMessages - s.resetSentMessages
|
||||
totalRecvMessages := s.numRecvMessages.Load()
|
||||
recvMessages := totalRecvMessages - s.resetRecvMessages
|
||||
log.Printf("Stats: sent=%d (%d/sec), recv=%d (%d/sec), delta=%d",
|
||||
totalSentMessages, sentMessages/perSec,
|
||||
totalRecvMessages, recvMessages/perSec,
|
||||
totalSentMessages-totalRecvMessages)
|
||||
s.reset(now)
|
||||
}
|
||||
|
||||
type MessagePayload struct {
|
||||
Now time.Time `json:"now"`
|
||||
}
|
||||
|
||||
func (m *MessagePayload) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
w.RawByte('{')
|
||||
w.RawString("\"now\":")
|
||||
w.Raw(m.Now.MarshalJSON())
|
||||
w.RawByte('}')
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
func (m *MessagePayload) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
r.Delim('{')
|
||||
for !r.IsDelim('}') {
|
||||
key := r.UnsafeFieldName(false)
|
||||
r.WantColon()
|
||||
switch key {
|
||||
case "now":
|
||||
if r.IsNull() {
|
||||
r.Skip()
|
||||
} else {
|
||||
if data := r.Raw(); r.Ok() {
|
||||
r.AddError((m.Now).UnmarshalJSON(data))
|
||||
}
|
||||
}
|
||||
default:
|
||||
r.SkipRecursive()
|
||||
}
|
||||
r.WantComma()
|
||||
}
|
||||
r.Delim('}')
|
||||
r.Consumed()
|
||||
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
type SignalingClient struct {
|
||||
readyWg *sync.WaitGroup
|
||||
cookie *signaling.SessionIdCodec
|
||||
readyWg *sync.WaitGroup // +checklocksignore: Only written to from constructor.
|
||||
|
||||
conn *websocket.Conn
|
||||
|
||||
|
|
@ -124,13 +137,16 @@ type SignalingClient struct {
|
|||
|
||||
stopChan chan struct{}
|
||||
|
||||
lock sync.Mutex
|
||||
privateSessionId string
|
||||
publicSessionId string
|
||||
userId string
|
||||
lock sync.Mutex
|
||||
// +checklocks:lock
|
||||
privateSessionId api.PrivateSessionId
|
||||
// +checklocks:lock
|
||||
publicSessionId api.PublicSessionId
|
||||
// +checklocks:lock
|
||||
userId string
|
||||
}
|
||||
|
||||
func NewSignalingClient(cookie *signaling.SessionIdCodec, url string, stats *Stats, readyWg *sync.WaitGroup, doneWg *sync.WaitGroup) (*SignalingClient, error) {
|
||||
func NewSignalingClient(url string, stats *Stats, readyWg *sync.WaitGroup, doneWg *sync.WaitGroup) (*SignalingClient, error) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -138,7 +154,6 @@ func NewSignalingClient(cookie *signaling.SessionIdCodec, url string, stats *Sta
|
|||
|
||||
client := &SignalingClient{
|
||||
readyWg: readyWg,
|
||||
cookie: cookie,
|
||||
|
||||
conn: conn,
|
||||
|
||||
|
|
@ -146,15 +161,8 @@ func NewSignalingClient(cookie *signaling.SessionIdCodec, url string, stats *Sta
|
|||
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
doneWg.Add(2)
|
||||
go func() {
|
||||
defer doneWg.Done()
|
||||
client.readPump()
|
||||
}()
|
||||
go func() {
|
||||
defer doneWg.Done()
|
||||
client.writePump()
|
||||
}()
|
||||
doneWg.Go(client.readPump)
|
||||
doneWg.Go(client.writePump)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +177,10 @@ func (c *SignalingClient) Close() {
|
|||
c.lock.Lock()
|
||||
c.publicSessionId = ""
|
||||
c.privateSessionId = ""
|
||||
c.writeInternal(&api.ClientMessage{
|
||||
Type: "bye",
|
||||
Bye: &api.ByeClientMessage{},
|
||||
})
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // nolint
|
||||
c.conn.Close()
|
||||
|
|
@ -176,7 +188,7 @@ func (c *SignalingClient) Close() {
|
|||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *SignalingClient) Send(message *signaling.ClientMessage) {
|
||||
func (c *SignalingClient) Send(message *api.ClientMessage) {
|
||||
c.lock.Lock()
|
||||
if c.conn == nil {
|
||||
c.lock.Unlock()
|
||||
|
|
@ -191,9 +203,11 @@ func (c *SignalingClient) Send(message *signaling.ClientMessage) {
|
|||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *SignalingClient) processMessage(message *signaling.ServerMessage) {
|
||||
func (c *SignalingClient) processMessage(message *api.ServerMessage) {
|
||||
c.stats.numRecvMessages.Add(1)
|
||||
switch message.Type {
|
||||
case "welcome":
|
||||
// Ignore welcome message.
|
||||
case "hello":
|
||||
c.processHelloMessage(message)
|
||||
case "message":
|
||||
|
|
@ -209,37 +223,25 @@ func (c *SignalingClient) processMessage(message *signaling.ServerMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *SignalingClient) privateToPublicSessionId(privateId string) string {
|
||||
data, err := c.cookie.DecodePrivate(privateId)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not decode private session id: %s", err))
|
||||
}
|
||||
publicId, err := c.cookie.EncodePublic(data)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not encode public id: %s", err))
|
||||
}
|
||||
return publicId
|
||||
}
|
||||
|
||||
func (c *SignalingClient) processHelloMessage(message *signaling.ServerMessage) {
|
||||
func (c *SignalingClient) processHelloMessage(message *api.ServerMessage) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.privateSessionId = message.Hello.ResumeId
|
||||
c.publicSessionId = c.privateToPublicSessionId(c.privateSessionId)
|
||||
c.publicSessionId = message.Hello.SessionId
|
||||
c.userId = message.Hello.UserId
|
||||
log.Printf("Registered as %s (userid %s)", c.privateSessionId, c.userId)
|
||||
c.readyWg.Done()
|
||||
}
|
||||
|
||||
func (c *SignalingClient) PublicSessionId() string {
|
||||
func (c *SignalingClient) PublicSessionId() api.PublicSessionId {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
return c.publicSessionId
|
||||
}
|
||||
|
||||
func (c *SignalingClient) processMessageMessage(message *signaling.ServerMessage) {
|
||||
func (c *SignalingClient) processMessageMessage(message *api.ServerMessage) {
|
||||
var msg MessagePayload
|
||||
if err := json.Unmarshal(message.Message.Data, &msg); err != nil {
|
||||
if err := msg.UnmarshalJSON(message.Message.Data); err != nil {
|
||||
log.Println("Error in unmarshal", err)
|
||||
return
|
||||
}
|
||||
|
|
@ -294,7 +296,9 @@ func (c *SignalingClient) readPump() {
|
|||
break
|
||||
}
|
||||
|
||||
var message signaling.ServerMessage
|
||||
c.stats.numRecvBytes.Add(uint64(decodeBuffer.Len()))
|
||||
|
||||
var message api.ServerMessage
|
||||
if err := message.UnmarshalJSON(decodeBuffer.Bytes()); err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
break
|
||||
|
|
@ -304,13 +308,14 @@ func (c *SignalingClient) readPump() {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *SignalingClient) writeInternal(message *signaling.ClientMessage) bool {
|
||||
func (c *SignalingClient) writeInternal(message *api.ClientMessage) bool {
|
||||
var closeData []byte
|
||||
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
var written int
|
||||
writer, err := c.conn.NextWriter(websocket.TextMessage)
|
||||
if err == nil {
|
||||
_, err = easyjson.MarshalToWriter(message, writer)
|
||||
written, err = easyjson.MarshalToWriter(message, writer)
|
||||
}
|
||||
if err != nil {
|
||||
if err == websocket.ErrCloseSent {
|
||||
|
|
@ -326,6 +331,9 @@ func (c *SignalingClient) writeInternal(message *signaling.ClientMessage) bool {
|
|||
|
||||
writer.Close()
|
||||
c.stats.numSentMessages.Add(1)
|
||||
if written > 0 {
|
||||
c.stats.numSentBytes.Add(uint64(written))
|
||||
}
|
||||
return true
|
||||
|
||||
close:
|
||||
|
|
@ -369,7 +377,7 @@ func (c *SignalingClient) writePump() {
|
|||
}
|
||||
|
||||
func (c *SignalingClient) SendMessages(clients []*SignalingClient) {
|
||||
sessionIds := make(map[*SignalingClient]string)
|
||||
sessionIds := make(map[*SignalingClient]api.PublicSessionId)
|
||||
for _, c := range clients {
|
||||
sessionIds[c] = c.PublicSessionId()
|
||||
}
|
||||
|
|
@ -387,11 +395,11 @@ func (c *SignalingClient) SendMessages(clients []*SignalingClient) {
|
|||
msgdata := MessagePayload{
|
||||
Now: now,
|
||||
}
|
||||
data, _ := json.Marshal(msgdata)
|
||||
msg := &signaling.ClientMessage{
|
||||
data, _ := msgdata.MarshalJSON()
|
||||
msg := &api.ClientMessage{
|
||||
Type: "message",
|
||||
Message: &signaling.MessageClientMessage{
|
||||
Recipient: signaling.MessageClientMessageRecipient{
|
||||
Message: &api.MessageClientMessage{
|
||||
Recipient: api.MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
SessionId: sessionIds[recipient],
|
||||
},
|
||||
|
|
@ -405,35 +413,35 @@ func (c *SignalingClient) SendMessages(clients []*SignalingClient) {
|
|||
}
|
||||
|
||||
func registerAuthHandler(router *mux.Router) {
|
||||
router.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
|
||||
router.HandleFunc("/ocs/v2.php/apps/spreed/api/v1/signaling/backend", func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Println("Error reading body:", err)
|
||||
return
|
||||
}
|
||||
|
||||
rnd := r.Header.Get(signaling.HeaderBackendSignalingRandom)
|
||||
checksum := r.Header.Get(signaling.HeaderBackendSignalingChecksum)
|
||||
rnd := r.Header.Get(talk.HeaderBackendSignalingRandom)
|
||||
checksum := r.Header.Get(talk.HeaderBackendSignalingChecksum)
|
||||
if rnd == "" || checksum == "" {
|
||||
log.Println("No checksum headers found")
|
||||
return
|
||||
}
|
||||
|
||||
if verify := signaling.CalculateBackendChecksum(rnd, body, backendSecret); verify != checksum {
|
||||
if verify := talk.CalculateBackendChecksum(rnd, body, backendSecret); verify != checksum {
|
||||
log.Println("Backend checksum verification failed")
|
||||
return
|
||||
}
|
||||
|
||||
var request signaling.BackendClientRequest
|
||||
var request talk.BackendClientRequest
|
||||
if err := request.UnmarshalJSON(body); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
response := &signaling.BackendClientResponse{
|
||||
response := &talk.BackendClientResponse{
|
||||
Type: "auth",
|
||||
Auth: &signaling.BackendClientAuthResponse{
|
||||
Version: signaling.BackendVersion,
|
||||
Auth: &talk.BackendClientAuthResponse{
|
||||
Version: talk.BackendVersion,
|
||||
UserId: "sample-user",
|
||||
},
|
||||
}
|
||||
|
|
@ -445,9 +453,9 @@ func registerAuthHandler(router *mux.Router) {
|
|||
}
|
||||
|
||||
rawdata := json.RawMessage(data)
|
||||
payload := &signaling.OcsResponse{
|
||||
Ocs: &signaling.OcsBody{
|
||||
Meta: signaling.OcsMeta{
|
||||
payload := &talk.OcsResponse{
|
||||
Ocs: &talk.OcsBody{
|
||||
Meta: talk.OcsMeta{
|
||||
Status: "ok",
|
||||
StatusCode: http.StatusOK,
|
||||
Message: http.StatusText(http.StatusOK),
|
||||
|
|
@ -488,38 +496,48 @@ func main() {
|
|||
flag.Parse()
|
||||
log.SetFlags(0)
|
||||
|
||||
config, err := goconf.ReadConfigFile(*config)
|
||||
if *showVersion {
|
||||
fmt.Printf("nextcloud-spreed-signaling-client version %s/%s\n", version, runtime.Version())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
cfg, err := goconf.ReadConfigFile(*configFlag)
|
||||
if err != nil {
|
||||
log.Fatal("Could not read configuration: ", err)
|
||||
}
|
||||
|
||||
secret, _ := config.GetString("backend", "secret")
|
||||
secret, _ := config.GetStringOptionWithEnv(cfg, "backend", "secret")
|
||||
backendSecret = []byte(secret)
|
||||
|
||||
hashKey, _ := config.GetString("sessions", "hashkey")
|
||||
switch len(hashKey) {
|
||||
case 32:
|
||||
case 64:
|
||||
default:
|
||||
log.Printf("WARNING: The sessions hash key should be 32 or 64 bytes but is %d bytes", len(hashKey))
|
||||
log.Printf("Using a maximum of %d CPUs", runtime.GOMAXPROCS(0))
|
||||
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatalf("Error writing CPU profile to %s: %s", *cpuprofile, err)
|
||||
}
|
||||
log.Printf("Writing CPU profile to %s ...", *cpuprofile)
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
blockKey, _ := config.GetString("sessions", "blockkey")
|
||||
blockBytes := []byte(blockKey)
|
||||
switch len(blockKey) {
|
||||
case 0:
|
||||
blockBytes = nil
|
||||
case 16:
|
||||
case 24:
|
||||
case 32:
|
||||
default:
|
||||
log.Fatalf("The sessions block key must be 16, 24 or 32 bytes but is %d bytes", len(blockKey))
|
||||
}
|
||||
cookie := signaling.NewSessionIdCodec([]byte(hashKey), blockBytes)
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err) // nolint (defer pprof.StopCPUProfile() will not run which is ok in case of errors)
|
||||
}
|
||||
|
||||
cpus := runtime.NumCPU()
|
||||
runtime.GOMAXPROCS(cpus)
|
||||
log.Printf("Using a maximum of %d CPUs", cpus)
|
||||
defer func() {
|
||||
log.Printf("Writing Memory profile to %s ...", *memprofile)
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Printf("Error writing Memory profile to %s: %s", *memprofile, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
|
@ -544,7 +562,7 @@ func main() {
|
|||
|
||||
urls := make([]url.URL, 0)
|
||||
urlstrings := make([]string, 0)
|
||||
for _, host := range strings.Split(*addr, ",") {
|
||||
for host := range internal.SplitEntries(*addr, ",") {
|
||||
u := url.URL{
|
||||
Scheme: "ws",
|
||||
Host: host,
|
||||
|
|
@ -568,19 +586,19 @@ func main() {
|
|||
var readyWg sync.WaitGroup
|
||||
|
||||
for i := 0; i < *maxClients; i++ {
|
||||
client, err := NewSignalingClient(cookie, urls[i%len(urls)].String(), stats, &readyWg, &doneWg)
|
||||
client, err := NewSignalingClient(urls[i%len(urls)].String(), stats, &readyWg, &doneWg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
readyWg.Add(1)
|
||||
|
||||
request := &signaling.ClientMessage{
|
||||
request := &api.ClientMessage{
|
||||
Type: "hello",
|
||||
Hello: &signaling.HelloClientMessage{
|
||||
Version: signaling.HelloVersionV1,
|
||||
Auth: &signaling.HelloClientMessageAuth{
|
||||
Url: backendUrl + "/auth",
|
||||
Hello: &api.HelloClientMessage{
|
||||
Version: api.HelloVersionV1,
|
||||
Auth: &api.HelloClientMessageAuth{
|
||||
Url: backendUrl,
|
||||
Params: json.RawMessage("{}"),
|
||||
},
|
||||
},
|
||||
|
|
@ -596,11 +614,9 @@ func main() {
|
|||
log.Println("All connections established")
|
||||
|
||||
for _, c := range clients {
|
||||
doneWg.Add(1)
|
||||
go func(c *SignalingClient) {
|
||||
defer doneWg.Done()
|
||||
doneWg.Go(func() {
|
||||
c.SendMessages(clients)
|
||||
}(c)
|
||||
})
|
||||
}
|
||||
|
||||
stats.start = time.Now()
|
||||
97
cmd/client/stats.go
Normal file
97
cmd/client/stats.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
numRecvMessages atomic.Int64
|
||||
numSentMessages atomic.Int64
|
||||
resetRecvMessages int64
|
||||
resetSentMessages int64
|
||||
numRecvBytes atomic.Uint64
|
||||
numSentBytes atomic.Uint64
|
||||
resetRecvBytes uint64
|
||||
resetSentBytes uint64
|
||||
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func (s *Stats) reset(start time.Time) {
|
||||
s.resetRecvMessages = s.numRecvMessages.Load()
|
||||
s.resetSentMessages = s.numSentMessages.Load()
|
||||
s.resetRecvBytes = s.numRecvBytes.Load()
|
||||
s.resetSentBytes = s.numSentBytes.Load()
|
||||
s.start = start
|
||||
}
|
||||
|
||||
type statsLogEntries struct {
|
||||
totalSentMessages int64
|
||||
sentMessagesPerSec int64
|
||||
sentBytesPerSec api.Bandwidth
|
||||
|
||||
totalRecvMessages int64
|
||||
recvMessagesPerSec int64
|
||||
recvBytesPerSec api.Bandwidth
|
||||
}
|
||||
|
||||
func (s *Stats) getLogEntries(now time.Time) *statsLogEntries {
|
||||
duration := now.Sub(s.start)
|
||||
perSec := int64(duration / time.Second)
|
||||
if perSec == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalSentMessages := s.numSentMessages.Load()
|
||||
sentMessages := totalSentMessages - s.resetSentMessages
|
||||
sentBytes := api.BandwidthFromBytes(s.numSentBytes.Load() - s.resetSentBytes)
|
||||
totalRecvMessages := s.numRecvMessages.Load()
|
||||
recvMessages := totalRecvMessages - s.resetRecvMessages
|
||||
recvBytes := api.BandwidthFromBytes(s.numRecvBytes.Load() - s.resetRecvBytes)
|
||||
|
||||
s.reset(now)
|
||||
return &statsLogEntries{
|
||||
totalSentMessages: totalSentMessages,
|
||||
sentMessagesPerSec: sentMessages / perSec,
|
||||
sentBytesPerSec: sentBytes,
|
||||
|
||||
totalRecvMessages: totalRecvMessages,
|
||||
recvMessagesPerSec: recvMessages / perSec,
|
||||
recvBytesPerSec: recvBytes,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stats) Log() {
|
||||
now := time.Now()
|
||||
if entries := s.getLogEntries(now); entries != nil {
|
||||
log.Printf("Stats: sent=%d (%d/sec, %s), recv=%d (%d/sec, %s), delta=%d",
|
||||
entries.totalSentMessages, entries.sentMessagesPerSec, entries.sentBytesPerSec,
|
||||
entries.totalRecvMessages, entries.recvMessagesPerSec, entries.recvBytesPerSec,
|
||||
entries.totalSentMessages-entries.totalRecvMessages)
|
||||
}
|
||||
}
|
||||
80
cmd/client/stats_test.go
Normal file
80
cmd/client/stats_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
)
|
||||
|
||||
func TestStats(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
var stats Stats
|
||||
assert.Nil(stats.getLogEntries(time.Time{}))
|
||||
now := time.Now()
|
||||
if entries := stats.getLogEntries(now); assert.NotNil(entries) {
|
||||
assert.EqualValues(0, entries.totalSentMessages)
|
||||
assert.EqualValues(0, entries.sentMessagesPerSec)
|
||||
assert.EqualValues(0, entries.sentBytesPerSec)
|
||||
|
||||
assert.EqualValues(0, entries.totalRecvMessages)
|
||||
assert.EqualValues(0, entries.recvMessagesPerSec)
|
||||
assert.EqualValues(0, entries.recvBytesPerSec)
|
||||
}
|
||||
|
||||
stats.numSentMessages.Add(10)
|
||||
stats.numSentBytes.Add((api.Bandwidth(20) * api.Kilobit).Bits())
|
||||
|
||||
stats.numRecvMessages.Add(30)
|
||||
stats.numRecvBytes.Add((api.Bandwidth(40) * api.Kilobit).Bits())
|
||||
|
||||
if entries := stats.getLogEntries(now.Add(time.Second)); assert.NotNil(entries) {
|
||||
assert.EqualValues(10, entries.totalSentMessages)
|
||||
assert.EqualValues(10, entries.sentMessagesPerSec)
|
||||
assert.EqualValues(20*1024*8, entries.sentBytesPerSec)
|
||||
|
||||
assert.EqualValues(30, entries.totalRecvMessages)
|
||||
assert.EqualValues(30, entries.recvMessagesPerSec)
|
||||
assert.EqualValues(40*1024*8, entries.recvBytesPerSec)
|
||||
}
|
||||
|
||||
stats.numSentMessages.Add(100)
|
||||
stats.numSentBytes.Add((api.Bandwidth(200) * api.Kilobit).Bits())
|
||||
|
||||
stats.numRecvMessages.Add(300)
|
||||
stats.numRecvBytes.Add((api.Bandwidth(400) * api.Kilobit).Bits())
|
||||
|
||||
if entries := stats.getLogEntries(now.Add(2 * time.Second)); assert.NotNil(entries) {
|
||||
assert.EqualValues(110, entries.totalSentMessages)
|
||||
assert.EqualValues(100, entries.sentMessagesPerSec)
|
||||
assert.EqualValues(200*1024*8, entries.sentBytesPerSec)
|
||||
|
||||
assert.EqualValues(330, entries.totalRecvMessages)
|
||||
assert.EqualValues(300, entries.recvMessagesPerSec)
|
||||
assert.EqualValues(400*1024*8, entries.recvBytesPerSec)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
|
@ -30,14 +31,15 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/config"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
signalinglog "github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -65,49 +67,52 @@ func main() {
|
|||
}
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
signal.Notify(sigChan, syscall.SIGHUP)
|
||||
signal.Notify(sigChan, syscall.SIGUSR1)
|
||||
|
||||
log.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid())
|
||||
stopCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
config, err := goconf.ReadConfigFile(*configFlag)
|
||||
logger := log.Default()
|
||||
stopCtx = signalinglog.NewLoggerContext(stopCtx, logger)
|
||||
|
||||
logger.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid())
|
||||
|
||||
cfg, err := goconf.ReadConfigFile(*configFlag)
|
||||
if err != nil {
|
||||
log.Fatal("Could not read configuration: ", err)
|
||||
logger.Fatal("Could not read configuration: ", err)
|
||||
}
|
||||
|
||||
cpus := runtime.NumCPU()
|
||||
runtime.GOMAXPROCS(cpus)
|
||||
log.Printf("Using a maximum of %d CPUs", cpus)
|
||||
logger.Printf("Using a maximum of %d CPUs", runtime.GOMAXPROCS(0))
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
proxy, err := NewProxyServer(r, version, config)
|
||||
proxy, err := NewProxyServer(stopCtx, r, version, cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
if err := proxy.Start(config); err != nil {
|
||||
log.Fatal(err)
|
||||
if err := proxy.Start(cfg); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
defer proxy.Stop()
|
||||
|
||||
if addr, _ := signaling.GetStringOptionWithEnv(config, "http", "listen"); addr != "" {
|
||||
readTimeout, _ := config.GetInt("http", "readtimeout")
|
||||
if addr, _ := config.GetStringOptionWithEnv(cfg, "http", "listen"); addr != "" {
|
||||
readTimeout, _ := cfg.GetInt("http", "readtimeout")
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = defaultReadTimeout
|
||||
}
|
||||
writeTimeout, _ := config.GetInt("http", "writetimeout")
|
||||
writeTimeout, _ := cfg.GetInt("http", "writetimeout")
|
||||
if writeTimeout <= 0 {
|
||||
writeTimeout = defaultWriteTimeout
|
||||
}
|
||||
|
||||
for _, address := range strings.Split(addr, " ") {
|
||||
for address := range internal.SplitEntries(addr, " ") {
|
||||
go func(address string) {
|
||||
log.Println("Listening on", address)
|
||||
logger.Println("Listening on", address)
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Fatal("Could not start listening: ", err)
|
||||
logger.Fatal("Could not start listening: ", err)
|
||||
}
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
|
|
@ -117,7 +122,7 @@ func main() {
|
|||
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||
}
|
||||
if err := srv.Serve(listener); err != nil {
|
||||
log.Fatal("Could not start server: ", err)
|
||||
logger.Fatal("Could not start server: ", err)
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
|
|
@ -126,24 +131,24 @@ func main() {
|
|||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-stopCtx.Done():
|
||||
logger.Println("Interrupted")
|
||||
break loop
|
||||
case sig := <-sigChan:
|
||||
switch sig {
|
||||
case os.Interrupt:
|
||||
log.Println("Interrupted")
|
||||
break loop
|
||||
case syscall.SIGHUP:
|
||||
log.Printf("Received SIGHUP, reloading %s", *configFlag)
|
||||
logger.Printf("Received SIGHUP, reloading %s", *configFlag)
|
||||
if config, err := goconf.ReadConfigFile(*configFlag); err != nil {
|
||||
log.Printf("Could not read configuration from %s: %s", *configFlag, err)
|
||||
logger.Printf("Could not read configuration from %s: %s", *configFlag, err)
|
||||
} else {
|
||||
proxy.Reload(config)
|
||||
}
|
||||
case syscall.SIGUSR1:
|
||||
log.Printf("Received SIGUSR1, scheduling server to shutdown")
|
||||
logger.Printf("Received SIGUSR1, scheduling server to shutdown")
|
||||
proxy.ScheduleShutdown()
|
||||
}
|
||||
case <-proxy.ShutdownChannel():
|
||||
log.Printf("All clients disconnected, shutting down")
|
||||
logger.Printf("All clients disconnected, shutting down")
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
|
@ -27,25 +27,35 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/client"
|
||||
)
|
||||
|
||||
type ProxyClient struct {
|
||||
signaling.Client
|
||||
client.Client
|
||||
|
||||
proxy *ProxyServer
|
||||
|
||||
session atomic.Pointer[ProxySession]
|
||||
}
|
||||
|
||||
func NewProxyClient(ctx context.Context, proxy *ProxyServer, conn *websocket.Conn, addr string) (*ProxyClient, error) {
|
||||
func NewProxyClient(ctx context.Context, proxy *ProxyServer, conn *websocket.Conn, addr string, agent string) (*ProxyClient, error) {
|
||||
client := &ProxyClient{
|
||||
proxy: proxy,
|
||||
}
|
||||
client.SetConn(ctx, conn, addr, client)
|
||||
client.SetConn(ctx, conn, addr, agent, false, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *ProxyClient) GetSessionId() api.PublicSessionId {
|
||||
if session := c.GetSession(); session != nil {
|
||||
return session.PublicId()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *ProxyClient) GetSession() *ProxySession {
|
||||
return c.session.Load()
|
||||
}
|
||||
|
|
@ -54,18 +64,18 @@ func (c *ProxyClient) SetSession(session *ProxySession) {
|
|||
c.session.Store(session)
|
||||
}
|
||||
|
||||
func (c *ProxyClient) OnClosed(client signaling.HandlerClient) {
|
||||
if session := c.GetSession(); session != nil {
|
||||
func (c *ProxyClient) OnClosed() {
|
||||
if session := c.session.Swap(nil); session != nil {
|
||||
session.MarkUsed()
|
||||
}
|
||||
c.proxy.clientClosed(&c.Client)
|
||||
c.proxy.clientClosed(c)
|
||||
}
|
||||
|
||||
func (c *ProxyClient) OnMessageReceived(client signaling.HandlerClient, data []byte) {
|
||||
func (c *ProxyClient) OnMessageReceived(data []byte) {
|
||||
c.proxy.processMessage(c, data)
|
||||
}
|
||||
|
||||
func (c *ProxyClient) OnRTTReceived(client signaling.HandlerClient, rtt time.Duration) {
|
||||
func (c *ProxyClient) OnRTTReceived(rtt time.Duration) {
|
||||
if session := c.GetSession(); session != nil {
|
||||
session.MarkUsed()
|
||||
}
|
||||
|
|
@ -27,7 +27,8 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
|
@ -38,12 +39,15 @@ import (
|
|||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/geoip"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
initialReconnectInterval = 1 * time.Second
|
||||
maxReconnectInterval = 32 * time.Second
|
||||
maxReconnectInterval = 16 * time.Second
|
||||
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
|
@ -56,41 +60,56 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrNotConnected = errors.New("not connected")
|
||||
ErrNotConnected = errors.New("not connected") // +checklocksignore: Global readonly variable.
|
||||
)
|
||||
|
||||
type RemoteConnection struct {
|
||||
logger log.Logger
|
||||
mu sync.Mutex
|
||||
p *ProxyServer
|
||||
url *url.URL
|
||||
conn *websocket.Conn
|
||||
closer *signaling.Closer
|
||||
closed atomic.Bool
|
||||
// +checklocks:mu
|
||||
conn *websocket.Conn
|
||||
closeCtx context.Context
|
||||
closeFunc context.CancelFunc // +checklocksignore: Only written to from constructor.
|
||||
|
||||
tokenId string
|
||||
tokenKey *rsa.PrivateKey
|
||||
tlsConfig *tls.Config
|
||||
|
||||
// +checklocks:mu
|
||||
connectedSince time.Time
|
||||
reconnectTimer *time.Timer
|
||||
reconnectInterval atomic.Int64
|
||||
|
||||
msgId atomic.Int64
|
||||
msgId atomic.Int64
|
||||
// +checklocks:mu
|
||||
helloMsgId string
|
||||
sessionId string
|
||||
// +checklocks:mu
|
||||
sessionId api.PublicSessionId
|
||||
// +checklocks:mu
|
||||
helloReceived bool
|
||||
|
||||
pendingMessages []*signaling.ProxyClientMessage
|
||||
messageCallbacks map[string]chan *signaling.ProxyServerMessage
|
||||
// +checklocks:mu
|
||||
pendingMessages []*proxy.ClientMessage
|
||||
// +checklocks:mu
|
||||
messageCallbacks map[string]chan *proxy.ServerMessage
|
||||
}
|
||||
|
||||
func NewRemoteConnection(proxyUrl string, tokenId string, tokenKey *rsa.PrivateKey, tlsConfig *tls.Config) (*RemoteConnection, error) {
|
||||
func NewRemoteConnection(p *ProxyServer, proxyUrl string, tokenId string, tokenKey *rsa.PrivateKey, tlsConfig *tls.Config) (*RemoteConnection, error) {
|
||||
u, err := url.Parse(proxyUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
closeCtx, closeFunc := context.WithCancel(context.Background())
|
||||
|
||||
result := &RemoteConnection{
|
||||
url: u,
|
||||
closer: signaling.NewCloser(),
|
||||
logger: p.logger,
|
||||
p: p,
|
||||
url: u,
|
||||
closeCtx: closeCtx,
|
||||
closeFunc: closeFunc,
|
||||
|
||||
tokenId: tokenId,
|
||||
tokenKey: tokenKey,
|
||||
|
|
@ -98,7 +117,7 @@ func NewRemoteConnection(proxyUrl string, tokenId string, tokenKey *rsa.PrivateK
|
|||
|
||||
reconnectTimer: time.NewTimer(0),
|
||||
|
||||
messageCallbacks: make(map[string]chan *signaling.ProxyServerMessage),
|
||||
messageCallbacks: make(map[string]chan *proxy.ServerMessage),
|
||||
}
|
||||
result.reconnectInterval.Store(int64(initialReconnectInterval))
|
||||
|
||||
|
|
@ -111,16 +130,23 @@ func (c *RemoteConnection) String() string {
|
|||
return c.url.String()
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) SessionId() api.PublicSessionId {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.sessionId
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) reconnect() {
|
||||
u, err := c.url.Parse("proxy")
|
||||
if err != nil {
|
||||
log.Printf("Could not resolve url to proxy at %s: %s", c, err)
|
||||
c.logger.Printf("Could not resolve url to proxy at %s: %s", c, err)
|
||||
c.scheduleReconnect()
|
||||
return
|
||||
}
|
||||
if u.Scheme == "http" {
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
u.Scheme = "ws"
|
||||
} else if u.Scheme == "https" {
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
}
|
||||
|
||||
|
|
@ -129,58 +155,81 @@ func (c *RemoteConnection) reconnect() {
|
|||
TLSClientConfig: c.tlsConfig,
|
||||
}
|
||||
|
||||
conn, _, err := dialer.DialContext(context.TODO(), u.String(), nil)
|
||||
conn, _, err := dialer.DialContext(c.closeCtx, u.String(), nil)
|
||||
if err != nil {
|
||||
log.Printf("Error connecting to proxy at %s: %s", c, err)
|
||||
c.logger.Printf("Error connecting to proxy at %s: %s", c, err)
|
||||
c.scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Connected to %s", c)
|
||||
c.closed.Store(false)
|
||||
c.logger.Printf("Connected to %s", c)
|
||||
|
||||
c.mu.Lock()
|
||||
if c.closeCtx.Err() != nil {
|
||||
// Closed while waiting for lock.
|
||||
c.mu.Unlock()
|
||||
if err := conn.Close(); err != nil {
|
||||
c.logger.Printf("Error closing connection to %s: %s", c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
c.connectedSince = time.Now()
|
||||
c.conn = conn
|
||||
c.mu.Unlock()
|
||||
|
||||
c.reconnectInterval.Store(int64(initialReconnectInterval))
|
||||
|
||||
if err := c.sendHello(); err != nil {
|
||||
log.Printf("Error sending hello request to proxy at %s: %s", c, err)
|
||||
if !c.sendReconnectHello() || !c.sendPing() {
|
||||
c.scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
if !c.sendPing() {
|
||||
return
|
||||
}
|
||||
|
||||
go c.readPump(conn)
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) scheduleReconnect() {
|
||||
if err := c.sendClose(); err != nil && err != ErrNotConnected {
|
||||
log.Printf("Could not send close message to %s: %s", c, err)
|
||||
func (c *RemoteConnection) sendReconnectHello() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if err := c.sendHello(c.closeCtx); err != nil {
|
||||
c.logger.Printf("Error sending hello request to proxy at %s: %s", c, err)
|
||||
return false
|
||||
}
|
||||
c.close()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) scheduleReconnect() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.scheduleReconnectLocked()
|
||||
}
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *RemoteConnection) scheduleReconnectLocked() {
|
||||
if err := c.sendCloseLocked(); err != nil && err != ErrNotConnected {
|
||||
c.logger.Printf("Could not send close message to %s: %s", c, err)
|
||||
}
|
||||
c.closeLocked()
|
||||
|
||||
interval := c.reconnectInterval.Load()
|
||||
c.reconnectTimer.Reset(time.Duration(interval))
|
||||
// Prevent all servers from reconnecting at the same time in case of an
|
||||
// interrupted connection to the proxy or a restart.
|
||||
jitter := rand.Int64N(interval) - (interval / 2)
|
||||
c.reconnectTimer.Reset(time.Duration(interval + jitter))
|
||||
|
||||
interval = interval * 2
|
||||
if interval > int64(maxReconnectInterval) {
|
||||
interval = int64(maxReconnectInterval)
|
||||
}
|
||||
interval = min(interval*2, int64(maxReconnectInterval))
|
||||
c.reconnectInterval.Store(interval)
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) sendHello() error {
|
||||
// +checklocks:c.mu
|
||||
func (c *RemoteConnection) sendHello(ctx context.Context) error {
|
||||
c.helloMsgId = strconv.FormatInt(c.msgId.Add(1), 10)
|
||||
msg := &signaling.ProxyClientMessage{
|
||||
msg := &proxy.ClientMessage{
|
||||
Id: c.helloMsgId,
|
||||
Type: "hello",
|
||||
Hello: &signaling.HelloProxyClientMessage{
|
||||
Hello: &proxy.HelloClientMessage{
|
||||
Version: "1.0",
|
||||
},
|
||||
}
|
||||
|
|
@ -195,13 +244,11 @@ func (c *RemoteConnection) sendHello() error {
|
|||
msg.Hello.Token = tokenString
|
||||
}
|
||||
|
||||
return c.SendMessage(msg)
|
||||
return c.sendMessageLocked(ctx, msg)
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) sendClose() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *RemoteConnection) sendCloseLocked() error {
|
||||
if c.conn == nil {
|
||||
return ErrNotConnected
|
||||
}
|
||||
|
|
@ -214,24 +261,39 @@ func (c *RemoteConnection) close() {
|
|||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.closeLocked()
|
||||
}
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *RemoteConnection) closeLocked() {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connectedSince = time.Time{}
|
||||
c.helloReceived = false
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.reconnectTimer.Stop()
|
||||
if c.conn == nil {
|
||||
|
||||
if c.closeCtx.Err() != nil {
|
||||
// Already closed
|
||||
return nil
|
||||
}
|
||||
|
||||
c.sendClose()
|
||||
err1 := c.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Time{})
|
||||
err2 := c.conn.Close()
|
||||
c.conn = nil
|
||||
c.closeFunc()
|
||||
var err1 error
|
||||
var err2 error
|
||||
if c.conn != nil {
|
||||
err1 = c.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Time{})
|
||||
err2 = c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connectedSince = time.Time{}
|
||||
c.helloReceived = false
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
|
|
@ -239,7 +301,7 @@ func (c *RemoteConnection) Close() error {
|
|||
}
|
||||
|
||||
func (c *RemoteConnection) createToken(subject string) (string, error) {
|
||||
claims := &signaling.TokenClaims{
|
||||
claims := &proxy.TokenClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: c.tokenId,
|
||||
|
|
@ -255,14 +317,15 @@ func (c *RemoteConnection) createToken(subject string) (string, error) {
|
|||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) SendMessage(msg *signaling.ProxyClientMessage) error {
|
||||
func (c *RemoteConnection) SendMessage(msg *proxy.ClientMessage) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.sendMessageLocked(context.Background(), msg)
|
||||
return c.sendMessageLocked(c.closeCtx, msg)
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) deferMessage(ctx context.Context, msg *signaling.ProxyClientMessage) {
|
||||
// +checklocks:c.mu
|
||||
func (c *RemoteConnection) deferMessage(ctx context.Context, msg *proxy.ClientMessage) {
|
||||
c.pendingMessages = append(c.pendingMessages, msg)
|
||||
if ctx.Done() != nil {
|
||||
go func() {
|
||||
|
|
@ -280,7 +343,8 @@ func (c *RemoteConnection) deferMessage(ctx context.Context, msg *signaling.Prox
|
|||
}
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) sendMessageLocked(ctx context.Context, msg *signaling.ProxyClientMessage) error {
|
||||
// +checklocks:c.mu
|
||||
func (c *RemoteConnection) sendMessageLocked(ctx context.Context, msg *proxy.ClientMessage) error {
|
||||
if c.conn == nil {
|
||||
// Defer until connected.
|
||||
c.deferMessage(ctx, msg)
|
||||
|
|
@ -299,7 +363,7 @@ func (c *RemoteConnection) sendMessageLocked(ctx context.Context, msg *signaling
|
|||
|
||||
func (c *RemoteConnection) readPump(conn *websocket.Conn) {
|
||||
defer func() {
|
||||
if !c.closed.Load() {
|
||||
if c.closeCtx.Err() == nil {
|
||||
c.scheduleReconnect()
|
||||
}
|
||||
}()
|
||||
|
|
@ -314,19 +378,21 @@ func (c *RemoteConnection) readPump(conn *websocket.Conn) {
|
|||
websocket.CloseNormalClosure,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseNoStatusReceived) {
|
||||
log.Printf("Error reading from %s: %v", c, err)
|
||||
if !errors.Is(err, net.ErrClosed) || c.closeCtx.Err() == nil {
|
||||
c.logger.Printf("Error reading from %s: %v", c, err)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if msgType != websocket.TextMessage {
|
||||
log.Printf("unexpected message type %q (%s)", msgType, string(msg))
|
||||
c.logger.Printf("unexpected message type %q (%s)", msgType, string(msg))
|
||||
continue
|
||||
}
|
||||
|
||||
var message signaling.ProxyServerMessage
|
||||
var message proxy.ServerMessage
|
||||
if err := json.Unmarshal(msg, &message); err != nil {
|
||||
log.Printf("could not decode message %s: %s", string(msg), err)
|
||||
c.logger.Printf("could not decode message %s: %s", string(msg), err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +419,7 @@ func (c *RemoteConnection) sendPing() bool {
|
|||
msg := strconv.FormatInt(now.UnixNano(), 10)
|
||||
c.conn.SetWriteDeadline(now.Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil {
|
||||
log.Printf("Could not send ping to proxy at %s: %v", c, err)
|
||||
c.logger.Printf("Could not send ping to proxy at %s: %v", c, err)
|
||||
go c.scheduleReconnect()
|
||||
return false
|
||||
}
|
||||
|
|
@ -374,44 +440,48 @@ func (c *RemoteConnection) writePump() {
|
|||
c.reconnect()
|
||||
case <-ticker.C:
|
||||
c.sendPing()
|
||||
case <-c.closer.C:
|
||||
case <-c.closeCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) processHello(msg *signaling.ProxyServerMessage) {
|
||||
func (c *RemoteConnection) processHello(msg *proxy.ServerMessage) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.helloMsgId = ""
|
||||
switch msg.Type {
|
||||
case "error":
|
||||
if msg.Error.Code == "no_such_session" {
|
||||
log.Printf("Session %s could not be resumed on %s, registering new", c.sessionId, c)
|
||||
c.logger.Printf("Session %s could not be resumed on %s, registering new", c.sessionId, c)
|
||||
c.sessionId = ""
|
||||
if err := c.sendHello(); err != nil {
|
||||
log.Printf("Could not send hello request to %s: %s", c, err)
|
||||
c.scheduleReconnect()
|
||||
if err := c.sendHello(c.closeCtx); err != nil {
|
||||
c.logger.Printf("Could not send hello request to %s: %s", c, err)
|
||||
c.scheduleReconnectLocked()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Hello connection to %s failed with %+v, reconnecting", c, msg.Error)
|
||||
c.scheduleReconnect()
|
||||
c.logger.Printf("Hello connection to %s failed with %+v, reconnecting", c, msg.Error)
|
||||
c.scheduleReconnectLocked()
|
||||
case "hello":
|
||||
resumed := c.sessionId == msg.Hello.SessionId
|
||||
c.sessionId = msg.Hello.SessionId
|
||||
country := ""
|
||||
c.helloReceived = true
|
||||
var country geoip.Country
|
||||
if msg.Hello.Server != nil {
|
||||
if country = msg.Hello.Server.Country; country != "" && !signaling.IsValidCountry(country) {
|
||||
log.Printf("Proxy %s sent invalid country %s in hello response", c, country)
|
||||
if country = msg.Hello.Server.Country; country != "" && !geoip.IsValidCountry(country) {
|
||||
c.logger.Printf("Proxy %s sent invalid country %s in hello response", c, country)
|
||||
country = ""
|
||||
}
|
||||
}
|
||||
if resumed {
|
||||
log.Printf("Resumed session %s on %s", c.sessionId, c)
|
||||
c.logger.Printf("Resumed session %s on %s", c.sessionId, c)
|
||||
} else if country != "" {
|
||||
log.Printf("Received session %s from %s (in %s)", c.sessionId, c, country)
|
||||
c.logger.Printf("Received session %s from %s (in %s)", c.sessionId, c, country)
|
||||
} else {
|
||||
log.Printf("Received session %s from %s", c.sessionId, c)
|
||||
c.logger.Printf("Received session %s from %s", c.sessionId, c)
|
||||
}
|
||||
|
||||
pending := c.pendingMessages
|
||||
|
|
@ -421,60 +491,94 @@ func (c *RemoteConnection) processHello(msg *signaling.ProxyServerMessage) {
|
|||
continue
|
||||
}
|
||||
|
||||
if err := c.sendMessageLocked(context.Background(), m); err != nil {
|
||||
log.Printf("Could not send pending message %+v to %s: %s", m, c, err)
|
||||
if err := c.sendMessageLocked(c.closeCtx, m); err != nil {
|
||||
c.logger.Printf("Could not send pending message %+v to %s: %s", m, c, err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Printf("Received unsupported hello response %+v from %s, reconnecting", msg, c)
|
||||
c.scheduleReconnect()
|
||||
c.logger.Printf("Received unsupported hello response %+v from %s, reconnecting", msg, c)
|
||||
c.scheduleReconnectLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) processMessage(msg *signaling.ProxyServerMessage) {
|
||||
if msg.Id != "" {
|
||||
c.mu.Lock()
|
||||
ch, found := c.messageCallbacks[msg.Id]
|
||||
if found {
|
||||
delete(c.messageCallbacks, msg.Id)
|
||||
c.mu.Unlock()
|
||||
ch <- msg
|
||||
return
|
||||
}
|
||||
func (c *RemoteConnection) handleCallback(msg *proxy.ServerMessage) bool {
|
||||
if msg.Id == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
ch, found := c.messageCallbacks[msg.Id]
|
||||
if !found {
|
||||
c.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
delete(c.messageCallbacks, msg.Id)
|
||||
c.mu.Unlock()
|
||||
|
||||
ch <- msg
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) processMessage(msg *proxy.ServerMessage) {
|
||||
if c.handleCallback(msg) {
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case "event":
|
||||
c.processEvent(msg)
|
||||
case "bye":
|
||||
c.logger.Printf("Connection to %s was closed: %s", c, msg.Bye.Reason)
|
||||
if msg.Bye.Reason == "session_expired" {
|
||||
// Don't try to resume expired session.
|
||||
c.mu.Lock()
|
||||
c.sessionId = ""
|
||||
c.mu.Unlock()
|
||||
}
|
||||
c.scheduleReconnect()
|
||||
default:
|
||||
log.Printf("Received unsupported message %+v from %s", msg, c)
|
||||
c.logger.Printf("Received unsupported message %+v from %s", msg, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) processEvent(msg *signaling.ProxyServerMessage) {
|
||||
func (c *RemoteConnection) processEvent(msg *proxy.ServerMessage) {
|
||||
switch msg.Event.Type {
|
||||
case "update-load":
|
||||
// Ignore
|
||||
case "publisher-closed":
|
||||
c.logger.Printf("Remote publisher %s was closed on %s", msg.Event.ClientId, c)
|
||||
c.p.RemotePublisherDeleted(api.PublicSessionId(msg.Event.ClientId))
|
||||
default:
|
||||
log.Printf("Received unsupported event %+v from %s", msg, c)
|
||||
c.logger.Printf("Received unsupported event %+v from %s", msg, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) RequestMessage(ctx context.Context, msg *signaling.ProxyClientMessage) (*signaling.ProxyServerMessage, error) {
|
||||
func (c *RemoteConnection) sendMessageWithCallbackLocked(ctx context.Context, msg *proxy.ClientMessage) (string, <-chan *proxy.ServerMessage, error) {
|
||||
msg.Id = strconv.FormatInt(c.msgId.Add(1), 10)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if err := c.sendMessageLocked(ctx, msg); err != nil {
|
||||
msg.Id = ""
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
ch := make(chan *proxy.ServerMessage, 1)
|
||||
c.messageCallbacks[msg.Id] = ch
|
||||
return msg.Id, ch, nil
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) RequestMessage(ctx context.Context, msg *proxy.ClientMessage) (*proxy.ServerMessage, error) {
|
||||
id, ch, err := c.sendMessageWithCallbackLocked(ctx, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch := make(chan *signaling.ProxyServerMessage, 1)
|
||||
c.messageCallbacks[msg.Id] = ch
|
||||
c.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
delete(c.messageCallbacks, msg.Id)
|
||||
defer c.mu.Unlock()
|
||||
delete(c.messageCallbacks, id)
|
||||
}()
|
||||
|
||||
select {
|
||||
|
|
@ -488,3 +592,15 @@ func (c *RemoteConnection) RequestMessage(ctx context.Context, msg *signaling.Pr
|
|||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) SendBye() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.sendMessageLocked(c.closeCtx, &proxy.ClientMessage{
|
||||
Type: "bye",
|
||||
})
|
||||
}
|
||||
216
cmd/proxy/proxy_remote_test.go
Normal file
216
cmd/proxy/proxy_remote_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/proxy"
|
||||
)
|
||||
|
||||
func (c *RemoteConnection) WaitForConnection(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Only used in tests, so a busy-loop should be fine.
|
||||
for c.conn == nil || c.connectedSince.IsZero() || !c.helloReceived {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
time.Sleep(time.Nanosecond)
|
||||
c.mu.Lock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RemoteConnection) WaitForDisconnect(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
initial := c.conn
|
||||
if initial == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only used in tests, so a busy-loop should be fine.
|
||||
for c.conn == initial {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
time.Sleep(time.Nanosecond)
|
||||
c.mu.Lock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_ProxyRemoteConnectionReconnect(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
server, key, httpserver := newProxyServerForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(conn.SendBye())
|
||||
assert.NoError(conn.Close())
|
||||
})
|
||||
|
||||
assert.NoError(conn.WaitForConnection(ctx))
|
||||
|
||||
// Closing the connection will reconnect automatically
|
||||
conn.mu.Lock()
|
||||
c := conn.conn
|
||||
conn.mu.Unlock()
|
||||
assert.NoError(c.Close())
|
||||
assert.NoError(conn.WaitForDisconnect(ctx))
|
||||
assert.NoError(conn.WaitForConnection(ctx))
|
||||
}
|
||||
|
||||
func Test_ProxyRemoteConnectionReconnectUnknownSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
server, key, httpserver := newProxyServerForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(conn.SendBye())
|
||||
assert.NoError(conn.Close())
|
||||
})
|
||||
|
||||
assert.NoError(conn.WaitForConnection(ctx))
|
||||
|
||||
// Closing the connection will reconnect automatically
|
||||
conn.mu.Lock()
|
||||
c := conn.conn
|
||||
sessionId := conn.sessionId
|
||||
conn.mu.Unlock()
|
||||
var sid uint64
|
||||
server.IterateSessions(func(session *ProxySession) {
|
||||
if session.PublicId() == sessionId {
|
||||
sid = session.Sid()
|
||||
}
|
||||
})
|
||||
require.NotEqualValues(0, sid)
|
||||
server.DeleteSession(sid)
|
||||
if err := c.Close(); err != nil {
|
||||
// If an error occurs while closing, it may only be "use of closed network
|
||||
// connection" because the "DeleteSession" might have already closed the
|
||||
// socket.
|
||||
assert.ErrorIs(err, net.ErrClosed)
|
||||
}
|
||||
assert.NoError(conn.WaitForDisconnect(ctx))
|
||||
assert.NoError(conn.WaitForConnection(ctx))
|
||||
assert.NotEqual(sessionId, conn.SessionId())
|
||||
}
|
||||
|
||||
func Test_ProxyRemoteConnectionReconnectExpiredSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
server, key, httpserver := newProxyServerForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(conn.SendBye())
|
||||
assert.NoError(conn.Close())
|
||||
})
|
||||
|
||||
assert.NoError(conn.WaitForConnection(ctx))
|
||||
|
||||
// Closing the connection will reconnect automatically
|
||||
conn.mu.Lock()
|
||||
sessionId := conn.sessionId
|
||||
conn.mu.Unlock()
|
||||
var session *ProxySession
|
||||
server.IterateSessions(func(sess *ProxySession) {
|
||||
if sess.PublicId() == sessionId {
|
||||
session = sess
|
||||
}
|
||||
})
|
||||
require.NotNil(session)
|
||||
session.Close()
|
||||
assert.NoError(conn.WaitForDisconnect(ctx))
|
||||
assert.NoError(conn.WaitForConnection(ctx))
|
||||
assert.NotEqual(sessionId, conn.SessionId())
|
||||
}
|
||||
|
||||
func Test_ProxyRemoteConnectionCreatePublisher(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
server, key, httpserver := newProxyServerForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(conn.SendBye())
|
||||
assert.NoError(conn.Close())
|
||||
})
|
||||
|
||||
publisherId := "the-publisher"
|
||||
hostname := "the-hostname"
|
||||
port := 1234
|
||||
rtcpPort := 2345
|
||||
|
||||
_, err = conn.RequestMessage(ctx, &proxy.ClientMessage{
|
||||
Type: "command",
|
||||
Command: &proxy.CommandClientMessage{
|
||||
Type: "publish-remote",
|
||||
ClientId: publisherId,
|
||||
Hostname: hostname,
|
||||
Port: port,
|
||||
RtcpPort: rtcpPort,
|
||||
},
|
||||
})
|
||||
assert.ErrorContains(err, UnknownClient.Error())
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -24,12 +24,14 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/proxy"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/sfu"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -38,49 +40,59 @@ const (
|
|||
)
|
||||
|
||||
type remotePublisherData struct {
|
||||
id api.PublicSessionId
|
||||
hostname string
|
||||
port int
|
||||
rtcpPort int
|
||||
}
|
||||
|
||||
type ProxySession struct {
|
||||
logger log.Logger
|
||||
proxy *ProxyServer
|
||||
id string
|
||||
id api.PublicSessionId
|
||||
sid uint64
|
||||
lastUsed atomic.Int64
|
||||
ctx context.Context
|
||||
closeFunc context.CancelFunc
|
||||
|
||||
clientLock sync.Mutex
|
||||
client *ProxyClient
|
||||
pendingMessages []*signaling.ProxyServerMessage
|
||||
clientLock sync.Mutex
|
||||
// +checklocks:clientLock
|
||||
client *ProxyClient
|
||||
// +checklocks:clientLock
|
||||
pendingMessages []*proxy.ServerMessage
|
||||
|
||||
publishersLock sync.Mutex
|
||||
publishers map[string]signaling.McuPublisher
|
||||
publisherIds map[signaling.McuPublisher]string
|
||||
// +checklocks:publishersLock
|
||||
publishers map[string]sfu.Publisher
|
||||
// +checklocks:publishersLock
|
||||
publisherIds map[sfu.Publisher]string
|
||||
|
||||
subscribersLock sync.Mutex
|
||||
subscribers map[string]signaling.McuSubscriber
|
||||
subscriberIds map[signaling.McuSubscriber]string
|
||||
// +checklocks:subscribersLock
|
||||
subscribers map[string]sfu.Subscriber
|
||||
// +checklocks:subscribersLock
|
||||
subscriberIds map[sfu.Subscriber]string
|
||||
|
||||
remotePublishersLock sync.Mutex
|
||||
remotePublishers map[signaling.McuPublisher]map[string]*remotePublisherData
|
||||
// +checklocks:remotePublishersLock
|
||||
remotePublishers map[sfu.RemoteAwarePublisher]map[string]*remotePublisherData
|
||||
}
|
||||
|
||||
func NewProxySession(proxy *ProxyServer, sid uint64, id string) *ProxySession {
|
||||
func NewProxySession(proxy *ProxyServer, sid uint64, id api.PublicSessionId) *ProxySession {
|
||||
ctx, closeFunc := context.WithCancel(context.Background())
|
||||
result := &ProxySession{
|
||||
logger: proxy.logger,
|
||||
proxy: proxy,
|
||||
id: id,
|
||||
sid: sid,
|
||||
ctx: ctx,
|
||||
closeFunc: closeFunc,
|
||||
|
||||
publishers: make(map[string]signaling.McuPublisher),
|
||||
publisherIds: make(map[signaling.McuPublisher]string),
|
||||
publishers: make(map[string]sfu.Publisher),
|
||||
publisherIds: make(map[sfu.Publisher]string),
|
||||
|
||||
subscribers: make(map[string]signaling.McuSubscriber),
|
||||
subscriberIds: make(map[signaling.McuSubscriber]string),
|
||||
subscribers: make(map[string]sfu.Subscriber),
|
||||
subscriberIds: make(map[sfu.Subscriber]string),
|
||||
}
|
||||
result.MarkUsed()
|
||||
return result
|
||||
|
|
@ -90,7 +102,7 @@ func (s *ProxySession) Context() context.Context {
|
|||
return s.ctx
|
||||
}
|
||||
|
||||
func (s *ProxySession) PublicId() string {
|
||||
func (s *ProxySession) PublicId() api.PublicSessionId {
|
||||
return s.id
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +117,7 @@ func (s *ProxySession) LastUsed() time.Time {
|
|||
|
||||
func (s *ProxySession) IsExpired() bool {
|
||||
expiresAt := s.LastUsed().Add(sessionExpirationTime)
|
||||
return expiresAt.Before(time.Now())
|
||||
return !expiresAt.After(time.Now())
|
||||
}
|
||||
|
||||
func (s *ProxySession) MarkUsed() {
|
||||
|
|
@ -120,9 +132,9 @@ func (s *ProxySession) Close() {
|
|||
if s.IsExpired() {
|
||||
reason = "session_expired"
|
||||
}
|
||||
prev.SendMessage(&signaling.ProxyServerMessage{
|
||||
prev.SendMessage(&proxy.ServerMessage{
|
||||
Type: "bye",
|
||||
Bye: &signaling.ByeProxyServerMessage{
|
||||
Bye: &proxy.ByeServerMessage{
|
||||
Reason: reason,
|
||||
},
|
||||
})
|
||||
|
|
@ -139,7 +151,7 @@ func (s *ProxySession) SetClient(client *ProxyClient) *ProxyClient {
|
|||
s.clientLock.Lock()
|
||||
prev := s.client
|
||||
s.client = client
|
||||
var messages []*signaling.ProxyServerMessage
|
||||
var messages []*proxy.ServerMessage
|
||||
if client != nil {
|
||||
messages, s.pendingMessages = s.pendingMessages, nil
|
||||
}
|
||||
|
|
@ -157,19 +169,19 @@ func (s *ProxySession) SetClient(client *ProxyClient) *ProxyClient {
|
|||
return prev
|
||||
}
|
||||
|
||||
func (s *ProxySession) OnUpdateOffer(client signaling.McuClient, offer map[string]interface{}) {
|
||||
func (s *ProxySession) OnUpdateOffer(client sfu.Client, offer api.StringMap) {
|
||||
id := s.proxy.GetClientId(client)
|
||||
if id == "" {
|
||||
log.Printf("Received offer %+v from unknown %s client %s (%+v)", offer, client.StreamType(), client.Id(), client)
|
||||
s.logger.Printf("Received offer %+v from unknown %s client %s (%+v)", offer, client.StreamType(), client.Id(), client)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &signaling.ProxyServerMessage{
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "payload",
|
||||
Payload: &signaling.PayloadProxyServerMessage{
|
||||
Payload: &proxy.PayloadServerMessage{
|
||||
Type: "offer",
|
||||
ClientId: id,
|
||||
Payload: map[string]interface{}{
|
||||
Payload: api.StringMap{
|
||||
"offer": offer,
|
||||
},
|
||||
},
|
||||
|
|
@ -177,19 +189,19 @@ func (s *ProxySession) OnUpdateOffer(client signaling.McuClient, offer map[strin
|
|||
s.sendMessage(msg)
|
||||
}
|
||||
|
||||
func (s *ProxySession) OnIceCandidate(client signaling.McuClient, candidate interface{}) {
|
||||
func (s *ProxySession) OnIceCandidate(client sfu.Client, candidate any) {
|
||||
id := s.proxy.GetClientId(client)
|
||||
if id == "" {
|
||||
log.Printf("Received candidate %+v from unknown %s client %s (%+v)", candidate, client.StreamType(), client.Id(), client)
|
||||
s.logger.Printf("Received candidate %+v from unknown %s client %s (%+v)", candidate, client.StreamType(), client.Id(), client)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &signaling.ProxyServerMessage{
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "payload",
|
||||
Payload: &signaling.PayloadProxyServerMessage{
|
||||
Payload: &proxy.PayloadServerMessage{
|
||||
Type: "candidate",
|
||||
ClientId: id,
|
||||
Payload: map[string]interface{}{
|
||||
Payload: api.StringMap{
|
||||
"candidate": candidate,
|
||||
},
|
||||
},
|
||||
|
|
@ -197,7 +209,7 @@ func (s *ProxySession) OnIceCandidate(client signaling.McuClient, candidate inte
|
|||
s.sendMessage(msg)
|
||||
}
|
||||
|
||||
func (s *ProxySession) sendMessage(message *signaling.ProxyServerMessage) {
|
||||
func (s *ProxySession) sendMessage(message *proxy.ServerMessage) {
|
||||
var client *ProxyClient
|
||||
s.clientLock.Lock()
|
||||
client = s.client
|
||||
|
|
@ -210,16 +222,16 @@ func (s *ProxySession) sendMessage(message *signaling.ProxyServerMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ProxySession) OnIceCompleted(client signaling.McuClient) {
|
||||
func (s *ProxySession) OnIceCompleted(client sfu.Client) {
|
||||
id := s.proxy.GetClientId(client)
|
||||
if id == "" {
|
||||
log.Printf("Received ice completed event from unknown %s client %s (%+v)", client.StreamType(), client.Id(), client)
|
||||
s.logger.Printf("Received ice completed event from unknown %s client %s (%+v)", client.StreamType(), client.Id(), client)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &signaling.ProxyServerMessage{
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "event",
|
||||
Event: &signaling.EventProxyServerMessage{
|
||||
Event: &proxy.EventServerMessage{
|
||||
Type: "ice-completed",
|
||||
ClientId: id,
|
||||
},
|
||||
|
|
@ -227,16 +239,16 @@ func (s *ProxySession) OnIceCompleted(client signaling.McuClient) {
|
|||
s.sendMessage(msg)
|
||||
}
|
||||
|
||||
func (s *ProxySession) SubscriberSidUpdated(subscriber signaling.McuSubscriber) {
|
||||
func (s *ProxySession) SubscriberSidUpdated(subscriber sfu.Subscriber) {
|
||||
id := s.proxy.GetClientId(subscriber)
|
||||
if id == "" {
|
||||
log.Printf("Received subscriber sid updated event from unknown %s subscriber %s (%+v)", subscriber.StreamType(), subscriber.Id(), subscriber)
|
||||
s.logger.Printf("Received subscriber sid updated event from unknown %s subscriber %s (%+v)", subscriber.StreamType(), subscriber.Id(), subscriber)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &signaling.ProxyServerMessage{
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "event",
|
||||
Event: &signaling.EventProxyServerMessage{
|
||||
Event: &proxy.EventServerMessage{
|
||||
Type: "subscriber-sid-updated",
|
||||
ClientId: id,
|
||||
Sid: subscriber.Sid(),
|
||||
|
|
@ -245,15 +257,15 @@ func (s *ProxySession) SubscriberSidUpdated(subscriber signaling.McuSubscriber)
|
|||
s.sendMessage(msg)
|
||||
}
|
||||
|
||||
func (s *ProxySession) PublisherClosed(publisher signaling.McuPublisher) {
|
||||
func (s *ProxySession) PublisherClosed(publisher sfu.Publisher) {
|
||||
if id := s.DeletePublisher(publisher); id != "" {
|
||||
if s.proxy.DeleteClient(id, publisher) {
|
||||
statsPublishersCurrent.WithLabelValues(string(publisher.StreamType())).Dec()
|
||||
}
|
||||
|
||||
msg := &signaling.ProxyServerMessage{
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "event",
|
||||
Event: &signaling.EventProxyServerMessage{
|
||||
Event: &proxy.EventServerMessage{
|
||||
Type: "publisher-closed",
|
||||
ClientId: id,
|
||||
},
|
||||
|
|
@ -262,15 +274,15 @@ func (s *ProxySession) PublisherClosed(publisher signaling.McuPublisher) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ProxySession) SubscriberClosed(subscriber signaling.McuSubscriber) {
|
||||
func (s *ProxySession) SubscriberClosed(subscriber sfu.Subscriber) {
|
||||
if id := s.DeleteSubscriber(subscriber); id != "" {
|
||||
if s.proxy.DeleteClient(id, subscriber) {
|
||||
statsSubscribersCurrent.WithLabelValues(string(subscriber.StreamType())).Dec()
|
||||
}
|
||||
|
||||
msg := &signaling.ProxyServerMessage{
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "event",
|
||||
Event: &signaling.EventProxyServerMessage{
|
||||
Event: &proxy.EventServerMessage{
|
||||
Type: "subscriber-closed",
|
||||
ClientId: id,
|
||||
},
|
||||
|
|
@ -279,7 +291,7 @@ func (s *ProxySession) SubscriberClosed(subscriber signaling.McuSubscriber) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ProxySession) StorePublisher(ctx context.Context, id string, publisher signaling.McuPublisher) {
|
||||
func (s *ProxySession) StorePublisher(ctx context.Context, id string, publisher sfu.Publisher) {
|
||||
s.publishersLock.Lock()
|
||||
defer s.publishersLock.Unlock()
|
||||
|
||||
|
|
@ -287,7 +299,7 @@ func (s *ProxySession) StorePublisher(ctx context.Context, id string, publisher
|
|||
s.publisherIds[publisher] = id
|
||||
}
|
||||
|
||||
func (s *ProxySession) DeletePublisher(publisher signaling.McuPublisher) string {
|
||||
func (s *ProxySession) DeletePublisher(publisher sfu.Publisher) string {
|
||||
s.publishersLock.Lock()
|
||||
defer s.publishersLock.Unlock()
|
||||
|
||||
|
|
@ -298,12 +310,16 @@ func (s *ProxySession) DeletePublisher(publisher signaling.McuPublisher) string
|
|||
|
||||
delete(s.publishers, id)
|
||||
delete(s.publisherIds, publisher)
|
||||
delete(s.remotePublishers, publisher)
|
||||
if rp, ok := publisher.(sfu.RemoteAwarePublisher); ok {
|
||||
s.remotePublishersLock.Lock()
|
||||
defer s.remotePublishersLock.Unlock()
|
||||
delete(s.remotePublishers, rp)
|
||||
}
|
||||
go s.proxy.PublisherDeleted(publisher)
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *ProxySession) StoreSubscriber(ctx context.Context, id string, subscriber signaling.McuSubscriber) {
|
||||
func (s *ProxySession) StoreSubscriber(ctx context.Context, id string, subscriber sfu.Subscriber) {
|
||||
s.subscribersLock.Lock()
|
||||
defer s.subscribersLock.Unlock()
|
||||
|
||||
|
|
@ -311,7 +327,7 @@ func (s *ProxySession) StoreSubscriber(ctx context.Context, id string, subscribe
|
|||
s.subscriberIds[subscriber] = id
|
||||
}
|
||||
|
||||
func (s *ProxySession) DeleteSubscriber(subscriber signaling.McuSubscriber) string {
|
||||
func (s *ProxySession) DeleteSubscriber(subscriber sfu.Subscriber) string {
|
||||
s.subscribersLock.Lock()
|
||||
defer s.subscribersLock.Unlock()
|
||||
|
||||
|
|
@ -329,7 +345,7 @@ func (s *ProxySession) clearPublishers() {
|
|||
s.publishersLock.Lock()
|
||||
defer s.publishersLock.Unlock()
|
||||
|
||||
go func(publishers map[string]signaling.McuPublisher) {
|
||||
go func(publishers map[string]sfu.Publisher) {
|
||||
for id, publisher := range publishers {
|
||||
if s.proxy.DeleteClient(id, publisher) {
|
||||
statsPublishersCurrent.WithLabelValues(string(publisher.StreamType())).Dec()
|
||||
|
|
@ -338,7 +354,7 @@ func (s *ProxySession) clearPublishers() {
|
|||
}
|
||||
}(s.publishers)
|
||||
// Can't use clear(...) here as the map is processed by the goroutine above.
|
||||
s.publishers = make(map[string]signaling.McuPublisher)
|
||||
s.publishers = make(map[string]sfu.Publisher)
|
||||
clear(s.publisherIds)
|
||||
}
|
||||
|
||||
|
|
@ -346,11 +362,11 @@ func (s *ProxySession) clearRemotePublishers() {
|
|||
s.remotePublishersLock.Lock()
|
||||
defer s.remotePublishersLock.Unlock()
|
||||
|
||||
go func(remotePublishers map[signaling.McuPublisher]map[string]*remotePublisherData) {
|
||||
go func(remotePublishers map[sfu.RemoteAwarePublisher]map[string]*remotePublisherData) {
|
||||
for publisher, entries := range remotePublishers {
|
||||
for _, data := range entries {
|
||||
if err := publisher.UnpublishRemote(context.Background(), s.PublicId(), data.hostname, data.port, data.rtcpPort); err != nil {
|
||||
log.Printf("Error unpublishing %s %s from remote %s: %s", publisher.StreamType(), publisher.Id(), data.hostname, err)
|
||||
s.logger.Printf("Error unpublishing %s %s from remote %s: %s", publisher.StreamType(), publisher.Id(), data.hostname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -359,10 +375,10 @@ func (s *ProxySession) clearRemotePublishers() {
|
|||
}
|
||||
|
||||
func (s *ProxySession) clearSubscribers() {
|
||||
s.publishersLock.Lock()
|
||||
defer s.publishersLock.Unlock()
|
||||
s.subscribersLock.Lock()
|
||||
defer s.subscribersLock.Unlock()
|
||||
|
||||
go func(subscribers map[string]signaling.McuSubscriber) {
|
||||
go func(subscribers map[string]sfu.Subscriber) {
|
||||
for id, subscriber := range subscribers {
|
||||
if s.proxy.DeleteClient(id, subscriber) {
|
||||
statsSubscribersCurrent.WithLabelValues(string(subscriber.StreamType())).Dec()
|
||||
|
|
@ -371,7 +387,7 @@ func (s *ProxySession) clearSubscribers() {
|
|||
}
|
||||
}(s.subscribers)
|
||||
// Can't use clear(...) here as the map is processed by the goroutine above.
|
||||
s.subscribers = make(map[string]signaling.McuSubscriber)
|
||||
s.subscribers = make(map[string]sfu.Subscriber)
|
||||
clear(s.subscriberIds)
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +397,7 @@ func (s *ProxySession) NotifyDisconnected() {
|
|||
s.clearRemotePublishers()
|
||||
}
|
||||
|
||||
func (s *ProxySession) AddRemotePublisher(publisher signaling.McuPublisher, hostname string, port int, rtcpPort int) bool {
|
||||
func (s *ProxySession) AddRemotePublisher(publisher sfu.RemoteAwarePublisher, hostname string, port int, rtcpPort int) bool {
|
||||
s.remotePublishersLock.Lock()
|
||||
defer s.remotePublishersLock.Unlock()
|
||||
|
||||
|
|
@ -389,7 +405,7 @@ func (s *ProxySession) AddRemotePublisher(publisher signaling.McuPublisher, host
|
|||
if !found {
|
||||
remote = make(map[string]*remotePublisherData)
|
||||
if s.remotePublishers == nil {
|
||||
s.remotePublishers = make(map[signaling.McuPublisher]map[string]*remotePublisherData)
|
||||
s.remotePublishers = make(map[sfu.RemoteAwarePublisher]map[string]*remotePublisherData)
|
||||
}
|
||||
s.remotePublishers[publisher] = remote
|
||||
}
|
||||
|
|
@ -400,6 +416,7 @@ func (s *ProxySession) AddRemotePublisher(publisher signaling.McuPublisher, host
|
|||
}
|
||||
|
||||
data := &remotePublisherData{
|
||||
id: publisher.PublisherId(),
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
rtcpPort: rtcpPort,
|
||||
|
|
@ -408,7 +425,7 @@ func (s *ProxySession) AddRemotePublisher(publisher signaling.McuPublisher, host
|
|||
return true
|
||||
}
|
||||
|
||||
func (s *ProxySession) RemoveRemotePublisher(publisher signaling.McuPublisher, hostname string, port int, rtcpPort int) {
|
||||
func (s *ProxySession) RemoveRemotePublisher(publisher sfu.RemoteAwarePublisher, hostname string, port int, rtcpPort int) {
|
||||
s.remotePublishersLock.Lock()
|
||||
defer s.remotePublishersLock.Unlock()
|
||||
|
||||
|
|
@ -427,9 +444,43 @@ func (s *ProxySession) RemoveRemotePublisher(publisher signaling.McuPublisher, h
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ProxySession) OnPublisherDeleted(publisher signaling.McuPublisher) {
|
||||
func (s *ProxySession) OnPublisherDeleted(publisher sfu.Publisher) {
|
||||
if publisher, ok := publisher.(sfu.RemoteAwarePublisher); ok {
|
||||
s.OnRemoteAwarePublisherDeleted(publisher)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProxySession) OnRemoteAwarePublisherDeleted(publisher sfu.RemoteAwarePublisher) {
|
||||
s.remotePublishersLock.Lock()
|
||||
defer s.remotePublishersLock.Unlock()
|
||||
|
||||
delete(s.remotePublishers, publisher)
|
||||
if entries, found := s.remotePublishers[publisher]; found {
|
||||
delete(s.remotePublishers, publisher)
|
||||
|
||||
for _, entry := range entries {
|
||||
msg := &proxy.ServerMessage{
|
||||
Type: "event",
|
||||
Event: &proxy.EventServerMessage{
|
||||
Type: "publisher-closed",
|
||||
ClientId: string(entry.id),
|
||||
},
|
||||
}
|
||||
s.sendMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProxySession) OnRemotePublisherDeleted(publisherId api.PublicSessionId) {
|
||||
s.subscribersLock.Lock()
|
||||
defer s.subscribersLock.Unlock()
|
||||
|
||||
for id, sub := range s.subscribers {
|
||||
if sub.Publisher() == publisherId {
|
||||
delete(s.subscribers, id)
|
||||
delete(s.subscriberIds, sub)
|
||||
|
||||
s.logger.Printf("Remote subscriber %s was closed, closing %s subscriber %s", publisherId, sub.StreamType(), sub.Id())
|
||||
go sub.Close(context.Background())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +86,12 @@ var (
|
|||
Name: "token_errors_total",
|
||||
Help: "The total number of token errors",
|
||||
}, []string{"reason"})
|
||||
statsLoadCurrent = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "proxy",
|
||||
Name: "load",
|
||||
Help: "The current load of the signaling proxy",
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
@ -99,4 +105,5 @@ func init() {
|
|||
prometheus.MustRegister(statsCommandMessagesTotal)
|
||||
prometheus.MustRegister(statsPayloadMessagesTotal)
|
||||
prometheus.MustRegister(statsTokenErrorsTotal)
|
||||
prometheus.MustRegister(statsLoadCurrent)
|
||||
}
|
||||
|
|
@ -34,7 +34,9 @@ import (
|
|||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -43,15 +45,16 @@ var (
|
|||
|
||||
type ProxyTestClient struct {
|
||||
t *testing.T
|
||||
assert *assert.Assertions
|
||||
assert *assert.Assertions // +checklocksignore: Only written to from constructor.
|
||||
require *require.Assertions
|
||||
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
conn *websocket.Conn
|
||||
messageChan chan []byte
|
||||
readErrorChan chan error
|
||||
|
||||
sessionId string
|
||||
sessionId api.PublicSessionId
|
||||
}
|
||||
|
||||
func NewProxyTestClient(ctx context.Context, t *testing.T, url string) *ProxyTestClient {
|
||||
|
|
@ -117,16 +120,16 @@ loop:
|
|||
}
|
||||
|
||||
func (c *ProxyTestClient) SendBye() error {
|
||||
hello := &signaling.ProxyClientMessage{
|
||||
hello := &proxy.ClientMessage{
|
||||
Id: "9876",
|
||||
Type: "bye",
|
||||
Bye: &signaling.ByeProxyClientMessage{},
|
||||
Bye: &proxy.ByeClientMessage{},
|
||||
}
|
||||
return c.WriteJSON(hello)
|
||||
}
|
||||
|
||||
func (c *ProxyTestClient) WriteJSON(data interface{}) error {
|
||||
if msg, ok := data.(*signaling.ProxyClientMessage); ok {
|
||||
func (c *ProxyTestClient) WriteJSON(data any) error {
|
||||
if msg, ok := data.(*proxy.ClientMessage); ok {
|
||||
if err := msg.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -137,11 +140,11 @@ func (c *ProxyTestClient) WriteJSON(data interface{}) error {
|
|||
return c.conn.WriteJSON(data)
|
||||
}
|
||||
|
||||
func (c *ProxyTestClient) RunUntilMessage(ctx context.Context) (message *signaling.ProxyServerMessage, err error) {
|
||||
func (c *ProxyTestClient) RunUntilMessage(ctx context.Context) (message *proxy.ServerMessage, err error) {
|
||||
select {
|
||||
case err = <-c.readErrorChan:
|
||||
case msg := <-c.messageChan:
|
||||
var m signaling.ProxyServerMessage
|
||||
var m proxy.ServerMessage
|
||||
if err = json.Unmarshal(msg, &m); err == nil {
|
||||
message = &m
|
||||
}
|
||||
|
|
@ -162,7 +165,7 @@ func checkUnexpectedClose(err error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkMessageType(message *signaling.ProxyServerMessage, expectedType string) error {
|
||||
func checkMessageType(message *proxy.ServerMessage, expectedType string) error {
|
||||
if message == nil {
|
||||
return ErrNoMessageReceived
|
||||
}
|
||||
|
|
@ -188,8 +191,8 @@ func checkMessageType(message *signaling.ProxyServerMessage, expectedType string
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *ProxyTestClient) SendHello(key interface{}) error {
|
||||
claims := &signaling.TokenClaims{
|
||||
func (c *ProxyTestClient) SendHello(key any) error {
|
||||
claims := &proxy.TokenClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)),
|
||||
Issuer: TokenIdForTest,
|
||||
|
|
@ -199,10 +202,10 @@ func (c *ProxyTestClient) SendHello(key interface{}) error {
|
|||
tokenString, err := token.SignedString(key)
|
||||
c.require.NoError(err)
|
||||
|
||||
hello := &signaling.ProxyClientMessage{
|
||||
hello := &proxy.ClientMessage{
|
||||
Id: "1234",
|
||||
Type: "hello",
|
||||
Hello: &signaling.HelloProxyClientMessage{
|
||||
Hello: &proxy.HelloClientMessage{
|
||||
Version: "1.0",
|
||||
Features: []string{},
|
||||
Token: tokenString,
|
||||
|
|
@ -211,7 +214,7 @@ func (c *ProxyTestClient) SendHello(key interface{}) error {
|
|||
return c.WriteJSON(hello)
|
||||
}
|
||||
|
||||
func (c *ProxyTestClient) RunUntilHello(ctx context.Context) (message *signaling.ProxyServerMessage, err error) {
|
||||
func (c *ProxyTestClient) RunUntilHello(ctx context.Context) (message *proxy.ServerMessage, err error) {
|
||||
if message, err = c.RunUntilMessage(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -225,7 +228,7 @@ func (c *ProxyTestClient) RunUntilHello(ctx context.Context) (message *signaling
|
|||
return message, nil
|
||||
}
|
||||
|
||||
func (c *ProxyTestClient) RunUntilLoad(ctx context.Context, load int64) (message *signaling.ProxyServerMessage, err error) {
|
||||
func (c *ProxyTestClient) RunUntilLoad(ctx context.Context, load uint64) (message *proxy.ServerMessage, err error) {
|
||||
if message, err = c.RunUntilMessage(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -244,8 +247,8 @@ func (c *ProxyTestClient) RunUntilLoad(ctx context.Context, load int64) (message
|
|||
return message, nil
|
||||
}
|
||||
|
||||
func (c *ProxyTestClient) SendCommand(command *signaling.CommandProxyClientMessage) error {
|
||||
message := &signaling.ProxyClientMessage{
|
||||
func (c *ProxyTestClient) SendCommand(command *proxy.CommandClientMessage) error {
|
||||
message := &proxy.ClientMessage{
|
||||
Id: "2345",
|
||||
Type: "command",
|
||||
Command: command,
|
||||
|
|
@ -24,8 +24,8 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
|
@ -33,7 +33,9 @@ import (
|
|||
"github.com/dlintw/goconf"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/container"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/etcd"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -46,25 +48,27 @@ type tokenCacheEntry struct {
|
|||
}
|
||||
|
||||
type tokensEtcd struct {
|
||||
client *signaling.EtcdClient
|
||||
logger log.Logger
|
||||
client etcd.Client
|
||||
|
||||
tokenFormats atomic.Value
|
||||
tokenCache *signaling.LruCache
|
||||
tokenCache *container.LruCache[*tokenCacheEntry]
|
||||
}
|
||||
|
||||
func NewProxyTokensEtcd(config *goconf.ConfigFile) (ProxyTokens, error) {
|
||||
client, err := signaling.NewEtcdClient(config, "tokens")
|
||||
func NewProxyTokensEtcd(logger log.Logger, config *goconf.ConfigFile) (ProxyTokens, error) {
|
||||
client, err := etcd.NewClient(logger, config, "tokens")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !client.IsConfigured() {
|
||||
return nil, fmt.Errorf("No etcd endpoints configured")
|
||||
return nil, errors.New("no etcd endpoints configured")
|
||||
}
|
||||
|
||||
result := &tokensEtcd{
|
||||
logger: logger,
|
||||
client: client,
|
||||
tokenCache: signaling.NewLruCache(tokenCacheSize),
|
||||
tokenCache: container.NewLruCache[*tokenCacheEntry](tokenCacheSize),
|
||||
}
|
||||
if err := result.load(config, false); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -94,11 +98,11 @@ func (t *tokensEtcd) getByKey(id string, key string) (*ProxyToken, error) {
|
|||
if len(resp.Kvs) == 0 {
|
||||
return nil, nil
|
||||
} else if len(resp.Kvs) > 1 {
|
||||
log.Printf("Received multiple keys for %s, using last", key)
|
||||
t.logger.Printf("Received multiple keys for %s, using last", key)
|
||||
}
|
||||
|
||||
keyValue := resp.Kvs[len(resp.Kvs)-1].Value
|
||||
cached, _ := t.tokenCache.Get(key).(*tokenCacheEntry)
|
||||
cached := t.tokenCache.Get(key)
|
||||
if cached == nil || !bytes.Equal(cached.keyValue, keyValue) {
|
||||
// Parsed public keys are cached to avoid the parse overhead.
|
||||
publicKey, err := jwt.ParseRSAPublicKeyFromPEM(keyValue)
|
||||
|
|
@ -123,7 +127,7 @@ func (t *tokensEtcd) Get(id string) (*ProxyToken, error) {
|
|||
for _, k := range t.getKeys(id) {
|
||||
token, err := t.getByKey(id, k)
|
||||
if err != nil {
|
||||
log.Printf("Could not get public key from %s for %s: %s", k, id, err)
|
||||
t.logger.Printf("Could not get public key from %s for %s: %s", k, id, err)
|
||||
continue
|
||||
} else if token == nil {
|
||||
continue
|
||||
|
|
@ -151,18 +155,18 @@ func (t *tokensEtcd) load(config *goconf.ConfigFile, ignoreErrors bool) error {
|
|||
}
|
||||
|
||||
t.tokenFormats.Store(tokenFormats)
|
||||
log.Printf("Using %v as token formats", tokenFormats)
|
||||
t.logger.Printf("Using %v as token formats", tokenFormats)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tokensEtcd) Reload(config *goconf.ConfigFile) {
|
||||
if err := t.load(config, true); err != nil {
|
||||
log.Printf("Error reloading etcd tokens: %s", err)
|
||||
t.logger.Printf("Error reloading etcd tokens: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tokensEtcd) Close() {
|
||||
if err := t.client.Close(); err != nil {
|
||||
log.Printf("Error while closing etcd client: %s", err)
|
||||
t.logger.Printf("Error while closing etcd client: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,13 +27,10 @@ import (
|
|||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
|
|
@ -41,38 +38,23 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"go.etcd.io/etcd/server/v3/embed"
|
||||
"go.etcd.io/etcd/server/v3/lease"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/test"
|
||||
)
|
||||
|
||||
var (
|
||||
etcdListenUrl = "http://localhost:8080"
|
||||
)
|
||||
|
||||
func isErrorAddressAlreadyInUse(err error) bool {
|
||||
var eOsSyscall *os.SyscallError
|
||||
if !errors.As(err, &eOsSyscall) {
|
||||
return false
|
||||
}
|
||||
var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr)
|
||||
if !errors.As(eOsSyscall, &errErrno) {
|
||||
return false
|
||||
}
|
||||
if errErrno == syscall.EADDRINUSE {
|
||||
return true
|
||||
}
|
||||
const WSAEADDRINUSE = 10048
|
||||
if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newEtcdForTesting(t *testing.T) *embed.Etcd {
|
||||
cfg := embed.NewConfig()
|
||||
cfg.Dir = t.TempDir()
|
||||
os.Chmod(cfg.Dir, 0700) // nolint
|
||||
cfg.LogLevel = "warn"
|
||||
cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)))
|
||||
|
||||
u, err := url.Parse(etcdListenUrl)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -89,7 +71,7 @@ func newEtcdForTesting(t *testing.T) *embed.Etcd {
|
|||
peerListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+2))
|
||||
cfg.ListenPeerUrls = []url.URL{*peerListener}
|
||||
etcd, err = embed.StartEtcd(cfg)
|
||||
if isErrorAddressAlreadyInUse(err) {
|
||||
if test.IsErrorAddressAlreadyInUse(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +97,8 @@ func newTokensEtcdForTesting(t *testing.T) (*tokensEtcd, *embed.Etcd) {
|
|||
cfg.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String())
|
||||
cfg.AddOption("tokens", "keyformat", "/%s, /testing/%s/key")
|
||||
|
||||
tokens, err := NewProxyTokensEtcd(cfg)
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
tokens, err := NewProxyTokensEtcd(logger, cfg)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
tokens.Close()
|
||||
|
|
@ -132,7 +115,7 @@ func storeKey(t *testing.T, etcd *embed.Etcd, key string, pubkey crypto.PublicKe
|
|||
data, err = x509.MarshalPKIXPublicKey(&pubkey)
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
require.Fail(t, "unknown key type %T in %+v", pubkey, pubkey)
|
||||
require.Fail(t, "unknown key type", "type %T in %+v", pubkey, pubkey)
|
||||
}
|
||||
|
||||
data = pem.EncodeToMemory(&pem.Block{
|
||||
|
|
@ -155,7 +138,7 @@ func generateAndSaveKey(t *testing.T, etcd *embed.Etcd, name string) *rsa.Privat
|
|||
}
|
||||
|
||||
func TestProxyTokensEtcd(t *testing.T) {
|
||||
signaling.CatchLogForTest(t)
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
tokens, etcd := newTokensEtcdForTesting(t)
|
||||
|
||||
|
|
@ -170,3 +153,34 @@ func TestProxyTokensEtcd(t *testing.T) {
|
|||
assert.True(key2.PublicKey.Equal(token.key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyTokensEtcdReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
tokens, etcd := newTokensEtcdForTesting(t)
|
||||
|
||||
key1 := generateAndSaveKey(t, etcd, "/foo")
|
||||
|
||||
if token, err := tokens.Get("foo"); assert.NoError(err) && assert.NotNil(token) {
|
||||
assert.True(key1.PublicKey.Equal(token.key))
|
||||
}
|
||||
|
||||
if token, err := tokens.Get("bar"); assert.NoError(err) {
|
||||
assert.Nil(token)
|
||||
}
|
||||
|
||||
cfg := goconf.NewConfigFile()
|
||||
cfg.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String())
|
||||
cfg.AddOption("tokens", "keyformat", "/reload/%s/key")
|
||||
|
||||
tokens.Reload(cfg)
|
||||
key2 := generateAndSaveKey(t, etcd, "/reload/bar/key")
|
||||
|
||||
if token, err := tokens.Get("foo"); assert.NoError(err) {
|
||||
assert.Nil(token)
|
||||
}
|
||||
|
||||
if token, err := tokens.Get("bar"); assert.NoError(err) && assert.NotNil(token) {
|
||||
assert.True(key2.PublicKey.Equal(token.key))
|
||||
}
|
||||
}
|
||||
|
|
@ -23,23 +23,26 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
signaling "github.com/strukturag/nextcloud-spreed-signaling"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/config"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
)
|
||||
|
||||
type tokensStatic struct {
|
||||
logger log.Logger
|
||||
tokenKeys atomic.Value
|
||||
}
|
||||
|
||||
func NewProxyTokensStatic(config *goconf.ConfigFile) (ProxyTokens, error) {
|
||||
result := &tokensStatic{}
|
||||
func NewProxyTokensStatic(logger log.Logger, config *goconf.ConfigFile) (ProxyTokens, error) {
|
||||
result := &tokensStatic{
|
||||
logger: logger,
|
||||
}
|
||||
if err := result.load(config, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -61,8 +64,8 @@ func (t *tokensStatic) Get(id string) (*ProxyToken, error) {
|
|||
return token, nil
|
||||
}
|
||||
|
||||
func (t *tokensStatic) load(config *goconf.ConfigFile, ignoreErrors bool) error {
|
||||
options, err := signaling.GetStringOptions(config, "tokens", ignoreErrors)
|
||||
func (t *tokensStatic) load(cfg *goconf.ConfigFile, ignoreErrors bool) error {
|
||||
options, err := config.GetStringOptions(cfg, "tokens", ignoreErrors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -71,29 +74,29 @@ func (t *tokensStatic) load(config *goconf.ConfigFile, ignoreErrors bool) error
|
|||
for id, filename := range options {
|
||||
if filename == "" {
|
||||
if !ignoreErrors {
|
||||
return fmt.Errorf("No filename given for token %s", id)
|
||||
return fmt.Errorf("no filename given for token %s", id)
|
||||
}
|
||||
|
||||
log.Printf("No filename given for token %s, ignoring", id)
|
||||
t.logger.Printf("No filename given for token %s, ignoring", id)
|
||||
continue
|
||||
}
|
||||
|
||||
keyData, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
if !ignoreErrors {
|
||||
return fmt.Errorf("Could not read public key from %s: %s", filename, err)
|
||||
return fmt.Errorf("could not read public key from %s: %w", filename, err)
|
||||
}
|
||||
|
||||
log.Printf("Could not read public key from %s, ignoring: %s", filename, err)
|
||||
t.logger.Printf("Could not read public key from %s, ignoring: %s", filename, err)
|
||||
continue
|
||||
}
|
||||
key, err := jwt.ParseRSAPublicKeyFromPEM(keyData)
|
||||
if err != nil {
|
||||
if !ignoreErrors {
|
||||
return fmt.Errorf("Could not parse public key from %s: %s", filename, err)
|
||||
return fmt.Errorf("could not parse public key from %s: %w", filename, err)
|
||||
}
|
||||
|
||||
log.Printf("Could not parse public key from %s, ignoring: %s", filename, err)
|
||||
t.logger.Printf("Could not parse public key from %s, ignoring: %s", filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -104,14 +107,14 @@ func (t *tokensStatic) load(config *goconf.ConfigFile, ignoreErrors bool) error
|
|||
}
|
||||
|
||||
if len(tokenKeys) == 0 {
|
||||
log.Printf("No token keys loaded")
|
||||
t.logger.Printf("No token keys loaded")
|
||||
} else {
|
||||
var keyIds []string
|
||||
for k := range tokenKeys {
|
||||
keyIds = append(keyIds, k)
|
||||
}
|
||||
sort.Strings(keyIds)
|
||||
log.Printf("Enabled token keys: %v", keyIds)
|
||||
slices.Sort(keyIds)
|
||||
t.logger.Printf("Enabled token keys: %v", keyIds)
|
||||
}
|
||||
t.setTokenKeys(tokenKeys)
|
||||
return nil
|
||||
|
|
@ -119,7 +122,7 @@ func (t *tokensStatic) load(config *goconf.ConfigFile, ignoreErrors bool) error
|
|||
|
||||
func (t *tokensStatic) Reload(config *goconf.ConfigFile) {
|
||||
if err := t.load(config, true); err != nil {
|
||||
log.Printf("Error reloading static tokens: %s", err)
|
||||
t.logger.Printf("Error reloading static tokens: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
185
cmd/proxy/proxy_tokens_static_test.go
Normal file
185
cmd/proxy/proxy_tokens_static_test.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
)
|
||||
|
||||
func TestStaticTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
filename := path.Join(t.TempDir(), "token.pub")
|
||||
|
||||
key1, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(err)
|
||||
require.NoError(internal.WritePublicKey(&key1.PublicKey, filename))
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("tokens", "foo", filename)
|
||||
|
||||
tokens, err := NewProxyTokensStatic(logger, config)
|
||||
require.NoError(err)
|
||||
|
||||
defer tokens.Close()
|
||||
|
||||
if token, err := tokens.Get("foo"); assert.NoError(err) {
|
||||
assert.Equal("foo", token.id)
|
||||
assert.True(key1.PublicKey.Equal(token.key))
|
||||
}
|
||||
|
||||
key2, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(err)
|
||||
require.NoError(internal.WritePublicKey(&key2.PublicKey, filename))
|
||||
|
||||
tokens.Reload(config)
|
||||
|
||||
if token, err := tokens.Get("foo"); assert.NoError(err) {
|
||||
assert.Equal("foo", token.id)
|
||||
assert.True(key2.PublicKey.Equal(token.key))
|
||||
}
|
||||
}
|
||||
|
||||
func testStaticTokensMissing(t *testing.T, reload bool) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
filename := path.Join(t.TempDir(), "token.pub")
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
config := goconf.NewConfigFile()
|
||||
if !reload {
|
||||
config.AddOption("tokens", "foo", filename)
|
||||
}
|
||||
|
||||
tokens, err := NewProxyTokensStatic(logger, config)
|
||||
if !reload {
|
||||
assert.ErrorIs(err, os.ErrNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
defer tokens.Close()
|
||||
|
||||
config.AddOption("tokens", "foo", filename)
|
||||
tokens.Reload(config)
|
||||
}
|
||||
|
||||
func TestStaticTokensMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testStaticTokensMissing(t, false)
|
||||
}
|
||||
|
||||
func TestStaticTokensMissingReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testStaticTokensMissing(t, true)
|
||||
}
|
||||
|
||||
func testStaticTokensEmpty(t *testing.T, reload bool) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
config := goconf.NewConfigFile()
|
||||
if !reload {
|
||||
config.AddOption("tokens", "foo", "")
|
||||
}
|
||||
|
||||
tokens, err := NewProxyTokensStatic(logger, config)
|
||||
if !reload {
|
||||
assert.ErrorContains(err, "no filename given")
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
defer tokens.Close()
|
||||
|
||||
config.AddOption("tokens", "foo", "")
|
||||
tokens.Reload(config)
|
||||
}
|
||||
|
||||
func TestStaticTokensEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testStaticTokensEmpty(t, false)
|
||||
}
|
||||
|
||||
func TestStaticTokensEmptyReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testStaticTokensEmpty(t, true)
|
||||
}
|
||||
|
||||
func testStaticTokensInvalidData(t *testing.T, reload bool) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
filename := path.Join(t.TempDir(), "token.pub")
|
||||
require.NoError(os.WriteFile(filename, []byte("invalid-key-data"), 0600))
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
config := goconf.NewConfigFile()
|
||||
if !reload {
|
||||
config.AddOption("tokens", "foo", filename)
|
||||
}
|
||||
|
||||
tokens, err := NewProxyTokensStatic(logger, config)
|
||||
if !reload {
|
||||
assert.ErrorContains(err, "could not parse public key")
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
defer tokens.Close()
|
||||
|
||||
config.AddOption("tokens", "foo", filename)
|
||||
tokens.Reload(config)
|
||||
}
|
||||
|
||||
func TestStaticTokensInvalidData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testStaticTokensInvalidData(t, false)
|
||||
}
|
||||
|
||||
func TestStaticTokensInvalidDataReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testStaticTokensInvalidData(t, true)
|
||||
}
|
||||
437
cmd/server/main.go
Normal file
437
cmd/server/main.go
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2017 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
runtimepprof "runtime/pprof"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/async/events"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/config"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/dns"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/etcd"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/grpc"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
signalinglog "github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/nats"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/server"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/sfu"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "unreleased"
|
||||
|
||||
configFlag = flag.String("config", "server.conf", "config file to use")
|
||||
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to file")
|
||||
|
||||
showVersion = flag.Bool("version", false, "show version and quit")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultReadTimeout = 15
|
||||
defaultWriteTimeout = 30
|
||||
|
||||
initialMcuRetry = time.Second
|
||||
maxMcuRetry = time.Second * 16
|
||||
|
||||
dnsMonitorInterval = time.Second
|
||||
)
|
||||
|
||||
func createListener(addr string) (net.Listener, error) {
|
||||
if addr[0] == '/' {
|
||||
os.Remove(addr)
|
||||
return net.Listen("unix", addr)
|
||||
}
|
||||
|
||||
return net.Listen("tcp", addr)
|
||||
}
|
||||
|
||||
func createTLSListener(addr string, certFile, keyFile string) (net.Listener, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
if addr[0] == '/' {
|
||||
os.Remove(addr)
|
||||
return tls.Listen("unix", addr, &config)
|
||||
}
|
||||
|
||||
return tls.Listen("tcp", addr, &config)
|
||||
}
|
||||
|
||||
type Listeners struct {
|
||||
logger signalinglog.Logger // +checklocksignore
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
listeners []net.Listener
|
||||
}
|
||||
|
||||
func (l *Listeners) Add(listener net.Listener) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.listeners = append(l.listeners, listener)
|
||||
}
|
||||
|
||||
func (l *Listeners) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
for _, listener := range l.listeners {
|
||||
if err := listener.Close(); err != nil {
|
||||
l.logger.Printf("Error closing listener %s: %s", listener.Addr(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Lshortfile)
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("nextcloud-spreed-signaling version %s/%s\n", version, runtime.Version())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGHUP)
|
||||
signal.Notify(sigChan, syscall.SIGUSR1)
|
||||
|
||||
stopCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
logger := log.Default()
|
||||
stopCtx = signalinglog.NewLoggerContext(stopCtx, logger)
|
||||
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
if err := runtimepprof.StartCPUProfile(f); err != nil {
|
||||
logger.Fatalf("Error writing CPU profile to %s: %s", *cpuprofile, err)
|
||||
}
|
||||
logger.Printf("Writing CPU profile to %s ...", *cpuprofile)
|
||||
defer runtimepprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
logger.Printf("Writing Memory profile to %s ...", *memprofile)
|
||||
runtime.GC()
|
||||
if err := runtimepprof.WriteHeapProfile(f); err != nil {
|
||||
logger.Printf("Error writing Memory profile to %s: %s", *memprofile, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
logger.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid())
|
||||
|
||||
cfg, err := goconf.ReadConfigFile(*configFlag)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not read configuration: ", err)
|
||||
}
|
||||
|
||||
logger.Printf("Using a maximum of %d CPUs", runtime.GOMAXPROCS(0))
|
||||
|
||||
server.RegisterStats()
|
||||
|
||||
natsUrl, _ := config.GetStringOptionWithEnv(cfg, "nats", "url")
|
||||
if natsUrl == "" {
|
||||
natsUrl = nats.DefaultURL
|
||||
}
|
||||
|
||||
events, err := events.NewAsyncEvents(stopCtx, natsUrl)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not create async events client: ", err)
|
||||
}
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := events.Close(ctx); err != nil {
|
||||
logger.Printf("Error closing events handler: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
dnsMonitor, err := dns.NewMonitor(logger, dnsMonitorInterval, nil)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not create DNS monitor: ", err)
|
||||
}
|
||||
if err := dnsMonitor.Start(); err != nil {
|
||||
logger.Fatal("Could not start DNS monitor: ", err)
|
||||
}
|
||||
defer dnsMonitor.Stop()
|
||||
|
||||
etcdClient, err := etcd.NewClient(logger, cfg, "mcu")
|
||||
if err != nil {
|
||||
logger.Fatalf("Could not create etcd client: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := etcdClient.Close(); err != nil {
|
||||
logger.Printf("Error while closing etcd client: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
rpcServer, err := grpc.NewServer(stopCtx, cfg, version)
|
||||
if err != nil {
|
||||
logger.Fatalf("Could not create RPC server: %s", err)
|
||||
}
|
||||
go func() {
|
||||
if err := rpcServer.Run(); err != nil {
|
||||
logger.Fatalf("Could not start RPC server: %s", err)
|
||||
}
|
||||
}()
|
||||
defer rpcServer.Close()
|
||||
|
||||
rpcClients, err := grpc.NewClients(stopCtx, cfg, etcdClient, dnsMonitor, version)
|
||||
if err != nil {
|
||||
logger.Fatalf("Could not create RPC clients: %s", err)
|
||||
}
|
||||
defer rpcClients.Close()
|
||||
|
||||
r := mux.NewRouter()
|
||||
hub, err := server.NewHub(stopCtx, cfg, events, rpcServer, rpcClients, etcdClient, r, version)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not create hub: ", err)
|
||||
}
|
||||
|
||||
mcuUrl, _ := config.GetStringOptionWithEnv(cfg, "mcu", "url")
|
||||
mcuType, _ := cfg.GetString("mcu", "type")
|
||||
if mcuType == "" && mcuUrl != "" {
|
||||
logger.Printf("WARNING: Old-style MCU configuration detected with url but no type, defaulting to type %s", sfu.TypeJanus)
|
||||
mcuType = sfu.TypeJanus
|
||||
} else if mcuType == sfu.TypeJanus && mcuUrl == "" {
|
||||
logger.Printf("WARNING: Old-style MCU configuration detected with type but no url, disabling")
|
||||
mcuType = ""
|
||||
}
|
||||
|
||||
if mcuType != "" {
|
||||
var mcu sfu.SFU
|
||||
mcuRetry := initialMcuRetry
|
||||
mcuRetryTimer := time.NewTimer(mcuRetry)
|
||||
mcuTypeLoop:
|
||||
for {
|
||||
// Context should be cancelled on signals but need a way to differentiate later.
|
||||
ctx := context.TODO()
|
||||
switch mcuType {
|
||||
case sfu.TypeJanus:
|
||||
mcu, err = janus.NewJanusSFU(ctx, mcuUrl, cfg)
|
||||
proxy.UnregisterStats()
|
||||
janus.RegisterStats()
|
||||
case sfu.TypeProxy:
|
||||
mcu, err = proxy.NewProxySFU(ctx, cfg, etcdClient, rpcClients, dnsMonitor)
|
||||
janus.UnregisterStats()
|
||||
proxy.RegisterStats()
|
||||
default:
|
||||
logger.Fatal("Unsupported MCU type: ", mcuType)
|
||||
}
|
||||
if err == nil {
|
||||
err = mcu.Start(ctx)
|
||||
if err != nil {
|
||||
logger.Printf("Could not create %s MCU: %s", mcuType, err)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
logger.Printf("Could not initialize %s MCU (%s) will retry in %s", mcuType, err, mcuRetry)
|
||||
mcuRetryTimer.Reset(mcuRetry)
|
||||
select {
|
||||
case <-stopCtx.Done():
|
||||
logger.Fatalf("Cancelled")
|
||||
case sig := <-sigChan:
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
logger.Printf("Received SIGHUP, reloading %s", *configFlag)
|
||||
if cfg, err = goconf.ReadConfigFile(*configFlag); err != nil {
|
||||
logger.Printf("Could not read configuration from %s: %s", *configFlag, err)
|
||||
} else {
|
||||
mcuUrl, _ = config.GetStringOptionWithEnv(cfg, "mcu", "url")
|
||||
mcuType, _ = cfg.GetString("mcu", "type")
|
||||
if mcuType == "" && mcuUrl != "" {
|
||||
logger.Printf("WARNING: Old-style MCU configuration detected with url but no type, defaulting to type %s", sfu.TypeJanus)
|
||||
mcuType = sfu.TypeJanus
|
||||
} else if mcuType == sfu.TypeJanus && mcuUrl == "" {
|
||||
logger.Printf("WARNING: Old-style MCU configuration detected with type but no url, disabling")
|
||||
mcuType = ""
|
||||
break mcuTypeLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-mcuRetryTimer.C:
|
||||
// Retry connection
|
||||
mcuRetry = min(mcuRetry*2, maxMcuRetry)
|
||||
}
|
||||
}
|
||||
if mcu != nil {
|
||||
defer mcu.Stop()
|
||||
|
||||
logger.Printf("Using %s MCU", mcuType)
|
||||
hub.SetMcu(mcu)
|
||||
}
|
||||
}
|
||||
|
||||
go hub.Run()
|
||||
defer hub.Stop()
|
||||
|
||||
server, err := server.NewBackendServer(stopCtx, cfg, hub, version)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not create backend server: ", err)
|
||||
}
|
||||
if err := server.Start(r); err != nil {
|
||||
logger.Fatal("Could not start backend server: ", err)
|
||||
}
|
||||
|
||||
listeners := Listeners{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if saddr, _ := config.GetStringOptionWithEnv(cfg, "https", "listen"); saddr != "" {
|
||||
cert, _ := cfg.GetString("https", "certificate")
|
||||
key, _ := cfg.GetString("https", "key")
|
||||
if cert == "" || key == "" {
|
||||
logger.Fatal("Need a certificate and key for the HTTPS listener")
|
||||
}
|
||||
|
||||
readTimeout, _ := cfg.GetInt("https", "readtimeout")
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = defaultReadTimeout
|
||||
}
|
||||
writeTimeout, _ := cfg.GetInt("https", "writetimeout")
|
||||
if writeTimeout <= 0 {
|
||||
writeTimeout = defaultWriteTimeout
|
||||
}
|
||||
for address := range internal.SplitEntries(saddr, " ") {
|
||||
go func(address string) {
|
||||
logger.Println("Listening on", address)
|
||||
listener, err := createTLSListener(address, cert, key)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not start listening: ", err)
|
||||
}
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
|
||||
ReadTimeout: time.Duration(readTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||
}
|
||||
listeners.Add(listener)
|
||||
if err := srv.Serve(listener); err != nil {
|
||||
if !hub.IsShutdownScheduled() || !errors.Is(err, net.ErrClosed) {
|
||||
logger.Fatal("Could not start server: ", err)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
}
|
||||
|
||||
if addr, _ := config.GetStringOptionWithEnv(cfg, "http", "listen"); addr != "" {
|
||||
readTimeout, _ := cfg.GetInt("http", "readtimeout")
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = defaultReadTimeout
|
||||
}
|
||||
writeTimeout, _ := cfg.GetInt("http", "writetimeout")
|
||||
if writeTimeout <= 0 {
|
||||
writeTimeout = defaultWriteTimeout
|
||||
}
|
||||
|
||||
for address := range internal.SplitEntries(addr, " ") {
|
||||
go func(address string) {
|
||||
logger.Println("Listening on", address)
|
||||
listener, err := createListener(address)
|
||||
if err != nil {
|
||||
logger.Fatal("Could not start listening: ", err)
|
||||
}
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: addr,
|
||||
|
||||
ReadTimeout: time.Duration(readTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||
}
|
||||
listeners.Add(listener)
|
||||
if err := srv.Serve(listener); err != nil {
|
||||
if !hub.IsShutdownScheduled() || !errors.Is(err, net.ErrClosed) {
|
||||
logger.Fatal("Could not start server: ", err)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
}
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-stopCtx.Done():
|
||||
logger.Println("Interrupted")
|
||||
break loop
|
||||
case sig := <-sigChan:
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
logger.Printf("Received SIGHUP, reloading %s", *configFlag)
|
||||
if config, err := goconf.ReadConfigFile(*configFlag); err != nil {
|
||||
logger.Printf("Could not read configuration from %s: %s", *configFlag, err)
|
||||
} else {
|
||||
hub.Reload(stopCtx, config)
|
||||
server.Reload(config)
|
||||
}
|
||||
case syscall.SIGUSR1:
|
||||
logger.Printf("Received SIGUSR1, scheduling server to shutdown")
|
||||
hub.ScheduleShutdown()
|
||||
listeners.Close()
|
||||
}
|
||||
case <-hub.ShutdownChannel():
|
||||
logger.Printf("All clients disconnected, shutting down")
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,15 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -71,8 +72,7 @@ func GetStringOptions(config *goconf.ConfigFile, section string, ignoreErrors bo
|
|||
continue
|
||||
}
|
||||
|
||||
var ge goconf.GetError
|
||||
if errors.As(err, &ge) && ge.Reason == goconf.OptionNotFound {
|
||||
if ge, ok := internal.AsErrorType[goconf.GetError](err); ok && ge.Reason == goconf.OptionNotFound {
|
||||
// Skip options from "default" section.
|
||||
continue
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -19,47 +19,48 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package container
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ConcurrentStringStringMap struct {
|
||||
sync.Mutex
|
||||
d map[string]string
|
||||
type ConcurrentMap[K comparable, V any] struct {
|
||||
mu sync.RWMutex
|
||||
// +checklocks:mu
|
||||
d map[K]V
|
||||
}
|
||||
|
||||
func (m *ConcurrentStringStringMap) Set(key, value string) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
func (m *ConcurrentMap[K, V]) Set(key K, value V) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.d == nil {
|
||||
m.d = make(map[string]string)
|
||||
m.d = make(map[K]V)
|
||||
}
|
||||
m.d[key] = value
|
||||
}
|
||||
|
||||
func (m *ConcurrentStringStringMap) Get(key string) (string, bool) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
s, found := m.d[key]
|
||||
return s, found
|
||||
}
|
||||
|
||||
func (m *ConcurrentStringStringMap) Del(key string) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
func (m *ConcurrentMap[K, V]) Del(key K) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.d, key)
|
||||
}
|
||||
|
||||
func (m *ConcurrentStringStringMap) Len() int {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
func (m *ConcurrentMap[K, V]) Len() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.d)
|
||||
}
|
||||
|
||||
func (m *ConcurrentStringStringMap) Clear() {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
func (m *ConcurrentMap[K, V]) Clear() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.d = nil
|
||||
}
|
||||
|
|
@ -19,9 +19,10 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package container
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
|
@ -30,8 +31,9 @@ import (
|
|||
)
|
||||
|
||||
func TestConcurrentStringStringMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
var m ConcurrentStringStringMap
|
||||
var m ConcurrentMap[string, string]
|
||||
assert.Equal(0, m.Len())
|
||||
v, found := m.Get("foo")
|
||||
assert.False(found, "Expected missing entry, got %s", v)
|
||||
|
|
@ -76,21 +78,22 @@ func TestConcurrentStringStringMap(t *testing.T) {
|
|||
var wg sync.WaitGroup
|
||||
concurrency := 100
|
||||
count := 1000
|
||||
for x := 0; x < concurrency; x++ {
|
||||
wg.Add(1)
|
||||
go func(x int) {
|
||||
defer wg.Done()
|
||||
|
||||
for x := range concurrency {
|
||||
wg.Go(func() {
|
||||
key := "key-" + strconv.Itoa(x)
|
||||
for y := 0; y < count; y = y + 1 {
|
||||
value := newRandomString(32)
|
||||
rnd := rand.Text()
|
||||
for y := range count {
|
||||
value := rnd + "-" + strconv.Itoa(y)
|
||||
m.Set(key, value)
|
||||
if v, found := m.Get(key); !assert.True(found, "Expected entry for key %s", key) ||
|
||||
!assert.Equal(value, v, "Unexpected value for key %s", key) {
|
||||
if v, found := m.Get(key); !found {
|
||||
assert.True(found, "Expected entry for key %s", key)
|
||||
return
|
||||
} else if v != value {
|
||||
assert.Equal(value, v, "Unexpected value for key %s", key)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(x)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
assert.Equal(concurrency, m.Len())
|
||||
|
|
@ -19,23 +19,26 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/internal"
|
||||
)
|
||||
|
||||
type AllowedIps struct {
|
||||
allowed []*net.IPNet
|
||||
type IPList struct {
|
||||
ips []*net.IPNet
|
||||
}
|
||||
|
||||
func (a *AllowedIps) String() string {
|
||||
func (a *IPList) String() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("[")
|
||||
for idx, n := range a.allowed {
|
||||
for idx, n := range a.ips {
|
||||
if idx > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
|
|
@ -45,18 +48,14 @@ func (a *AllowedIps) String() string {
|
|||
return b.String()
|
||||
}
|
||||
|
||||
func (a *AllowedIps) Empty() bool {
|
||||
return len(a.allowed) == 0
|
||||
func (a *IPList) Empty() bool {
|
||||
return len(a.ips) == 0
|
||||
}
|
||||
|
||||
func (a *AllowedIps) Allowed(ip net.IP) bool {
|
||||
for _, i := range a.allowed {
|
||||
if i.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
func (a *IPList) Contains(ip net.IP) bool {
|
||||
return slices.ContainsFunc(a.ips, func(n *net.IPNet) bool {
|
||||
return n.Contains(ip)
|
||||
})
|
||||
}
|
||||
|
||||
func parseIPNet(s string) (*net.IPNet, error) {
|
||||
|
|
@ -81,35 +80,36 @@ func parseIPNet(s string) (*net.IPNet, error) {
|
|||
return ipnet, nil
|
||||
}
|
||||
|
||||
func ParseAllowedIps(allowed string) (*AllowedIps, error) {
|
||||
func ParseIPList(allowed string) (*IPList, error) {
|
||||
var allowedIps []*net.IPNet
|
||||
for _, ip := range strings.Split(allowed, ",") {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip != "" {
|
||||
i, err := parseIPNet(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedIps = append(allowedIps, i)
|
||||
for ip := range internal.SplitEntries(allowed, ",") {
|
||||
i, err := parseIPNet(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedIps = append(allowedIps, i)
|
||||
}
|
||||
|
||||
result := &AllowedIps{
|
||||
allowed: allowedIps,
|
||||
result := &IPList{
|
||||
ips: allowedIps,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func DefaultAllowedIps() *AllowedIps {
|
||||
func DefaultAllowedIPs() *IPList {
|
||||
allowedIps := []*net.IPNet{
|
||||
{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Mask: net.CIDRMask(32, 32),
|
||||
},
|
||||
{
|
||||
IP: net.ParseIP("::1"),
|
||||
Mask: net.CIDRMask(128, 128),
|
||||
},
|
||||
}
|
||||
|
||||
result := &AllowedIps{
|
||||
allowed: allowedIps,
|
||||
result := &IPList{
|
||||
ips: allowedIps,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -118,6 +118,7 @@ var (
|
|||
privateIpNets = []string{
|
||||
// Loopback addresses.
|
||||
"127.0.0.0/8",
|
||||
"::1",
|
||||
// Private addresses.
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
|
|
@ -125,8 +126,8 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
func DefaultPrivateIps() *AllowedIps {
|
||||
allowed, err := ParseAllowedIps(strings.Join(privateIpNets, ","))
|
||||
func DefaultPrivateIPs() *IPList {
|
||||
allowed, err := ParseIPList(strings.Join(privateIpNets, ","))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not parse private ips %+v: %w", privateIpNets, err))
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package container
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
|
@ -29,39 +29,67 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAllowedIps(t *testing.T) {
|
||||
func TestIPList(t *testing.T) {
|
||||
t.Parallel()
|
||||
require := require.New(t)
|
||||
a, err := ParseAllowedIps("127.0.0.1, 192.168.0.1, 192.168.1.1/24")
|
||||
a, err := ParseIPList("127.0.0.1, 192.168.0.1, 192.168.1.1/24")
|
||||
require.NoError(err)
|
||||
require.False(a.Empty())
|
||||
require.Equal(`[127.0.0.1/32, 192.168.0.1/32, 192.168.1.0/24]`, a.String())
|
||||
|
||||
allowed := []string{
|
||||
contained := []string{
|
||||
"127.0.0.1",
|
||||
"192.168.0.1",
|
||||
"192.168.1.1",
|
||||
"192.168.1.100",
|
||||
}
|
||||
notAllowed := []string{
|
||||
notContained := []string{
|
||||
"192.168.0.2",
|
||||
"10.1.2.3",
|
||||
}
|
||||
|
||||
for _, addr := range allowed {
|
||||
for _, addr := range contained {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
if ip := net.ParseIP(addr); assert.NotNil(ip, "error parsing %s", addr) {
|
||||
assert.True(a.Allowed(ip), "should allow %s", addr)
|
||||
assert.True(a.Contains(ip), "should contain %s", addr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, addr := range notAllowed {
|
||||
for _, addr := range notContained {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
if ip := net.ParseIP(addr); assert.NotNil(ip, "error parsing %s", addr) {
|
||||
assert.False(a.Allowed(ip), "should not allow %s", addr)
|
||||
assert.False(a.Contains(ip), "should not contain %s", addr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAllowedIPs(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
ips := DefaultAllowedIPs()
|
||||
assert.True(ips.Contains(net.ParseIP("127.0.0.1")))
|
||||
assert.False(ips.Contains(net.ParseIP("127.1.0.1")))
|
||||
assert.True(ips.Contains(net.ParseIP("::1")))
|
||||
assert.False(ips.Contains(net.ParseIP("1.1.1.1")))
|
||||
}
|
||||
|
||||
func TestDefaultPrivateIPs(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
ips := DefaultPrivateIPs()
|
||||
assert.True(ips.Contains(net.ParseIP("127.0.0.1")))
|
||||
assert.True(ips.Contains(net.ParseIP("127.1.0.1")))
|
||||
assert.True(ips.Contains(net.ParseIP("::1")))
|
||||
assert.True(ips.Contains(net.ParseIP("10.1.2.3")))
|
||||
assert.True(ips.Contains(net.ParseIP("172.16.17.18")))
|
||||
assert.True(ips.Contains(net.ParseIP("192.168.10.20")))
|
||||
assert.False(ips.Contains(net.ParseIP("1.1.1.1")))
|
||||
}
|
||||
|
|
@ -19,43 +19,45 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package container
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type cacheEntry struct {
|
||||
type cacheEntry[T any] struct {
|
||||
key string
|
||||
value interface{}
|
||||
value T
|
||||
}
|
||||
|
||||
type LruCache struct {
|
||||
size int
|
||||
mu sync.Mutex
|
||||
type LruCache[T any] struct {
|
||||
size int // +checklocksignore: Only written to from constructor.
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
entries *list.List
|
||||
data map[string]*list.Element
|
||||
// +checklocks:mu
|
||||
data map[string]*list.Element
|
||||
}
|
||||
|
||||
func NewLruCache(size int) *LruCache {
|
||||
return &LruCache{
|
||||
func NewLruCache[T any](size int) *LruCache[T] {
|
||||
return &LruCache[T]{
|
||||
size: size,
|
||||
entries: list.New(),
|
||||
data: make(map[string]*list.Element),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) Set(key string, value interface{}) {
|
||||
func (c *LruCache[T]) Set(key string, value T) {
|
||||
c.mu.Lock()
|
||||
if v, found := c.data[key]; found {
|
||||
c.entries.MoveToFront(v)
|
||||
v.Value.(*cacheEntry).value = value
|
||||
v.Value.(*cacheEntry[T]).value = value
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
v := c.entries.PushFront(&cacheEntry{
|
||||
v := c.entries.PushFront(&cacheEntry[T]{
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
|
|
@ -66,20 +68,21 @@ func (c *LruCache) Set(key string, value interface{}) {
|
|||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *LruCache) Get(key string) interface{} {
|
||||
func (c *LruCache[T]) Get(key string) T {
|
||||
c.mu.Lock()
|
||||
if v, found := c.data[key]; found {
|
||||
c.entries.MoveToFront(v)
|
||||
value := v.Value.(*cacheEntry).value
|
||||
value := v.Value.(*cacheEntry[T]).value
|
||||
c.mu.Unlock()
|
||||
return value
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
var defaultValue T
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (c *LruCache) Remove(key string) {
|
||||
func (c *LruCache[T]) Remove(key string) {
|
||||
c.mu.Lock()
|
||||
if v, found := c.data[key]; found {
|
||||
c.removeElement(v)
|
||||
|
|
@ -87,26 +90,28 @@ func (c *LruCache) Remove(key string) {
|
|||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *LruCache) removeOldestLocked() {
|
||||
// +checklocks:c.mu
|
||||
func (c *LruCache[T]) removeOldestLocked() {
|
||||
v := c.entries.Back()
|
||||
if v != nil {
|
||||
c.removeElement(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) RemoveOldest() {
|
||||
func (c *LruCache[T]) RemoveOldest() {
|
||||
c.mu.Lock()
|
||||
c.removeOldestLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *LruCache) removeElement(e *list.Element) {
|
||||
// +checklocks:c.mu
|
||||
func (c *LruCache[T]) removeElement(e *list.Element) {
|
||||
c.entries.Remove(e)
|
||||
entry := e.Value.(*cacheEntry)
|
||||
entry := e.Value.(*cacheEntry[T])
|
||||
delete(c.data, entry.key)
|
||||
}
|
||||
|
||||
func (c *LruCache) Len() int {
|
||||
func (c *LruCache[T]) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.entries.Len()
|
||||
|
|
@ -19,109 +19,101 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLruUnbound(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
lru := NewLruCache(0)
|
||||
lru := NewLruCache[int](0)
|
||||
count := 10
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
lru.Set(key, i)
|
||||
}
|
||||
assert.Equal(count, lru.Len())
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
if value := lru.Get(key); assert.NotNil(value, "No value found for %s", key) {
|
||||
assert.EqualValues(i, value)
|
||||
}
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
value := lru.Get(key)
|
||||
assert.Equal(i, value, "Failed for %s", key)
|
||||
}
|
||||
// The first key ("0") is now the oldest.
|
||||
lru.RemoveOldest()
|
||||
assert.Equal(count-1, lru.Len())
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
value := lru.Get(key)
|
||||
if i == 0 {
|
||||
assert.Nil(value, "The value for key %s should have been removed", key)
|
||||
continue
|
||||
} else if assert.NotNil(value, "No value found for %s", key) {
|
||||
assert.EqualValues(i, value)
|
||||
}
|
||||
assert.Equal(i, value, "Failed for %s", key)
|
||||
}
|
||||
|
||||
// NOTE: Key "0" no longer exists below, so make sure to not set it again.
|
||||
|
||||
// Using the same keys will update the ordering.
|
||||
for i := count - 1; i >= 1; i-- {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
key := strconv.Itoa(i)
|
||||
lru.Set(key, i)
|
||||
}
|
||||
assert.Equal(count-1, lru.Len())
|
||||
// NOTE: The same ordering as the Set calls above.
|
||||
for i := count - 1; i >= 1; i-- {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
if value := lru.Get(key); assert.NotNil(value, "No value found for %s", key) {
|
||||
assert.EqualValues(i, value)
|
||||
}
|
||||
key := strconv.Itoa(i)
|
||||
value := lru.Get(key)
|
||||
assert.Equal(i, value, "Failed for %s", key)
|
||||
}
|
||||
|
||||
// The last key ("9") is now the oldest.
|
||||
lru.RemoveOldest()
|
||||
assert.Equal(count-2, lru.Len())
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
value := lru.Get(key)
|
||||
if i == 0 || i == count-1 {
|
||||
assert.Nil(value, "The value for key %s should have been removed", key)
|
||||
continue
|
||||
} else if assert.NotNil(value, "No value found for %s", key) {
|
||||
assert.EqualValues(i, value)
|
||||
assert.Equal(0, value, "The value for key %s should have been removed", key)
|
||||
} else {
|
||||
assert.Equal(i, value, "Failed for %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an arbitrary key from the cache
|
||||
key := fmt.Sprintf("%d", count/2)
|
||||
key := strconv.Itoa(count / 2)
|
||||
lru.Remove(key)
|
||||
assert.Equal(count-3, lru.Len())
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
value := lru.Get(key)
|
||||
if i == 0 || i == count-1 || i == count/2 {
|
||||
assert.Nil(value, "The value for key %s should have been removed", key)
|
||||
continue
|
||||
} else if assert.NotNil(value, "No value found for %s", key) {
|
||||
assert.EqualValues(i, value)
|
||||
assert.Equal(0, value, "The value for key %s should have been removed", key)
|
||||
} else {
|
||||
assert.Equal(i, value, "Failed for %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLruBound(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
size := 2
|
||||
lru := NewLruCache(size)
|
||||
lru := NewLruCache[int](size)
|
||||
count := 10
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
lru.Set(key, i)
|
||||
}
|
||||
assert.Equal(size, lru.Len())
|
||||
// Only the last "size" entries have been stored.
|
||||
for i := 0; i < count; i++ {
|
||||
key := fmt.Sprintf("%d", i)
|
||||
for i := range count {
|
||||
key := strconv.Itoa(i)
|
||||
value := lru.Get(key)
|
||||
if i < count-size {
|
||||
assert.Nil(value, "The value for key %s should have been removed", key)
|
||||
continue
|
||||
} else if assert.NotNil(value, "No value found for %s", key) {
|
||||
assert.EqualValues(i, value)
|
||||
assert.Equal(0, value, "The value for key %s should have been removed", key)
|
||||
} else {
|
||||
assert.Equal(i, value, "Failed for %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
2
dist/init/systemd/signaling.service
vendored
2
dist/init/systemd/signaling.service
vendored
|
|
@ -12,7 +12,7 @@ ConfigurationDirectory=signaling
|
|||
|
||||
# Hardening - see systemd.exec(5)
|
||||
CapabilityBoundingSet=
|
||||
ExecPaths=/usr/bin/signaling /usr/lib
|
||||
ExecPaths=/usr/bin/signaling /usr/lib /usr/lib64
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
NoExecPaths=/
|
||||
|
|
|
|||
71
dns/internal/mock_lookup.go
Normal file
71
dns/internal/mock_lookup.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MockLookup struct {
|
||||
sync.RWMutex
|
||||
|
||||
// +checklocks:RWMutex
|
||||
ips map[string][]net.IP
|
||||
}
|
||||
|
||||
func NewMockLookup() *MockLookup {
|
||||
mock := &MockLookup{
|
||||
ips: make(map[string][]net.IP),
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (m *MockLookup) Set(host string, ips []net.IP) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
m.ips[host] = ips
|
||||
}
|
||||
|
||||
func (m *MockLookup) Get(host string) []net.IP {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.ips[host]
|
||||
}
|
||||
|
||||
func (m *MockLookup) Lookup(host string) ([]net.IP, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
ips, found := m.ips[host]
|
||||
if !found {
|
||||
return nil, &net.DNSError{
|
||||
Err: "could not resolve " + host,
|
||||
Name: host,
|
||||
IsNotFound: true,
|
||||
}
|
||||
}
|
||||
|
||||
return append([]net.IP{}, ips...), nil
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 struktur AG
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
|
|
@ -19,50 +19,40 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChannelWaiters(t *testing.T) {
|
||||
var waiters ChannelWaiters
|
||||
func TestMockLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ch1 := make(chan struct{}, 1)
|
||||
id1 := waiters.Add(ch1)
|
||||
defer waiters.Remove(id1)
|
||||
assert := assert.New(t)
|
||||
|
||||
ch2 := make(chan struct{}, 1)
|
||||
id2 := waiters.Add(ch2)
|
||||
defer waiters.Remove(id2)
|
||||
host1 := "domain1.invalid"
|
||||
host2 := "domain2.invalid"
|
||||
|
||||
waiters.Wakeup()
|
||||
<-ch1
|
||||
<-ch2
|
||||
lookup := NewMockLookup()
|
||||
assert.Empty(lookup.Get(host1))
|
||||
assert.Empty(lookup.Get(host2))
|
||||
|
||||
select {
|
||||
case <-ch1:
|
||||
assert.Fail(t, "should have not received another event")
|
||||
case <-ch2:
|
||||
assert.Fail(t, "should have not received another event")
|
||||
default:
|
||||
ips := []net.IP{
|
||||
net.ParseIP("1.2.3.4"),
|
||||
}
|
||||
lookup.Set(host1, ips)
|
||||
assert.Equal(ips, lookup.Get(host1))
|
||||
assert.Empty(lookup.Get(host2))
|
||||
|
||||
ch3 := make(chan struct{}, 1)
|
||||
id3 := waiters.Add(ch3)
|
||||
waiters.Remove(id3)
|
||||
|
||||
// Multiple wakeups work even without processing.
|
||||
waiters.Wakeup()
|
||||
waiters.Wakeup()
|
||||
waiters.Wakeup()
|
||||
<-ch1
|
||||
<-ch2
|
||||
select {
|
||||
case <-ch3:
|
||||
assert.Fail(t, "should have not received another event")
|
||||
default:
|
||||
if resolved, err := lookup.Lookup(host1); assert.NoError(err) {
|
||||
assert.Equal(ips, resolved)
|
||||
}
|
||||
var de *net.DNSError
|
||||
if resolved, err := lookup.Lookup(host2); assert.ErrorAs(err, &de, "expected error, got %+v", resolved) {
|
||||
assert.True(de.IsNotFound)
|
||||
assert.Equal(host2, de.Name)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,49 +19,64 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
lookupDnsMonitorIP = net.LookupIP
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDnsMonitorInterval = time.Second
|
||||
defaultMonitorInterval = time.Second
|
||||
)
|
||||
|
||||
type DnsMonitorCallback = func(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP)
|
||||
type MonitorCallback = func(entry *MonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP)
|
||||
|
||||
type DnsMonitorEntry struct {
|
||||
entry atomic.Pointer[dnsMonitorEntry]
|
||||
type MonitorEntry struct {
|
||||
entry atomic.Pointer[monitorEntry]
|
||||
url string
|
||||
callback DnsMonitorCallback
|
||||
callback MonitorCallback
|
||||
}
|
||||
|
||||
func (e *DnsMonitorEntry) URL() string {
|
||||
func (e *MonitorEntry) URL() string {
|
||||
return e.url
|
||||
}
|
||||
|
||||
type dnsMonitorEntry struct {
|
||||
type monitorEntry struct {
|
||||
hostname string
|
||||
hostIP net.IP
|
||||
|
||||
mu sync.Mutex
|
||||
ips []net.IP
|
||||
entries map[*DnsMonitorEntry]bool
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
ips []net.IP
|
||||
// +checklocks:mu
|
||||
entries map[*MonitorEntry]bool
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) setIPs(ips []net.IP, fromIP bool) {
|
||||
func (e *monitorEntry) clearRemoved() bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
deleted := false
|
||||
for entry := range e.entries {
|
||||
if entry.entry.Load() == nil {
|
||||
delete(e.entries, entry)
|
||||
deleted = true
|
||||
}
|
||||
}
|
||||
|
||||
return deleted && len(e.entries) == 0
|
||||
}
|
||||
|
||||
func (e *monitorEntry) setIPs(ips []net.IP, fromIP bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
|
|
@ -94,7 +109,7 @@ func (e *dnsMonitorEntry) setIPs(ips []net.IP, fromIP bool) {
|
|||
found := false
|
||||
for idx, newIP := range ips {
|
||||
if oldIP.Equal(newIP) {
|
||||
ips = append(ips[:idx], ips[idx+1:]...)
|
||||
ips = slices.Delete(ips, idx, idx+1)
|
||||
found = true
|
||||
keepIPs = append(keepIPs, oldIP)
|
||||
newIPs = append(newIPs, oldIP)
|
||||
|
|
@ -118,14 +133,14 @@ func (e *dnsMonitorEntry) setIPs(ips []net.IP, fromIP bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) addEntry(entry *DnsMonitorEntry) {
|
||||
func (e *monitorEntry) addEntry(entry *MonitorEntry) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.entries[entry] = true
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) removeEntry(entry *DnsMonitorEntry) bool {
|
||||
func (e *monitorEntry) removeEntry(entry *MonitorEntry) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
|
|
@ -133,14 +148,19 @@ func (e *dnsMonitorEntry) removeEntry(entry *DnsMonitorEntry) bool {
|
|||
return len(e.entries) == 0
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) runCallbacks(all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
||||
// +checklocks:e.mu
|
||||
func (e *monitorEntry) runCallbacks(all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
||||
for entry := range e.entries {
|
||||
entry.callback(entry, all, add, keep, remove)
|
||||
}
|
||||
}
|
||||
|
||||
type DnsMonitor struct {
|
||||
interval time.Duration
|
||||
type MonitorLookupFunc func(hostname string) ([]net.IP, error)
|
||||
|
||||
type Monitor struct {
|
||||
logger log.Logger
|
||||
interval time.Duration
|
||||
lookupFunc MonitorLookupFunc
|
||||
|
||||
stopCtx context.Context
|
||||
stopFunc func()
|
||||
|
|
@ -148,46 +168,48 @@ type DnsMonitor struct {
|
|||
|
||||
mu sync.RWMutex
|
||||
cond *sync.Cond
|
||||
hostnames map[string]*dnsMonitorEntry
|
||||
hostnames map[string]*monitorEntry
|
||||
|
||||
hasRemoved atomic.Bool
|
||||
|
||||
// Can be overwritten from tests.
|
||||
checkHostnames func()
|
||||
tickerWaiting atomic.Bool
|
||||
hasRemoved atomic.Bool
|
||||
}
|
||||
|
||||
func NewDnsMonitor(interval time.Duration) (*DnsMonitor, error) {
|
||||
func NewMonitor(logger log.Logger, interval time.Duration, lookupFunc MonitorLookupFunc) (*Monitor, error) {
|
||||
if interval < 0 {
|
||||
interval = defaultDnsMonitorInterval
|
||||
interval = defaultMonitorInterval
|
||||
}
|
||||
if lookupFunc == nil {
|
||||
lookupFunc = net.LookupIP
|
||||
}
|
||||
|
||||
stopCtx, stopFunc := context.WithCancel(context.Background())
|
||||
monitor := &DnsMonitor{
|
||||
interval: interval,
|
||||
monitor := &Monitor{
|
||||
logger: logger,
|
||||
interval: interval,
|
||||
lookupFunc: lookupFunc,
|
||||
|
||||
stopCtx: stopCtx,
|
||||
stopFunc: stopFunc,
|
||||
stopped: make(chan struct{}),
|
||||
|
||||
hostnames: make(map[string]*dnsMonitorEntry),
|
||||
hostnames: make(map[string]*monitorEntry),
|
||||
}
|
||||
monitor.cond = sync.NewCond(&monitor.mu)
|
||||
monitor.checkHostnames = monitor.doCheckHostnames
|
||||
return monitor, nil
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Start() error {
|
||||
func (m *Monitor) Start() error {
|
||||
go m.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Stop() {
|
||||
func (m *Monitor) Stop() {
|
||||
m.stopFunc()
|
||||
m.cond.Signal()
|
||||
<-m.stopped
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Add(target string, callback DnsMonitorCallback) (*DnsMonitorEntry, error) {
|
||||
func (m *Monitor) Add(target string, callback MonitorCallback) (*MonitorEntry, error) {
|
||||
var hostname string
|
||||
if strings.Contains(target, "://") {
|
||||
// Full URL passed.
|
||||
|
|
@ -207,17 +229,17 @@ func (m *DnsMonitor) Add(target string, callback DnsMonitorCallback) (*DnsMonito
|
|||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
e := &DnsMonitorEntry{
|
||||
e := &MonitorEntry{
|
||||
url: target,
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
entry, found := m.hostnames[hostname]
|
||||
if !found {
|
||||
entry = &dnsMonitorEntry{
|
||||
entry = &monitorEntry{
|
||||
hostname: hostname,
|
||||
hostIP: net.ParseIP(hostname),
|
||||
entries: make(map[*DnsMonitorEntry]bool),
|
||||
entries: make(map[*MonitorEntry]bool),
|
||||
}
|
||||
m.hostnames[hostname] = entry
|
||||
}
|
||||
|
|
@ -227,7 +249,7 @@ func (m *DnsMonitor) Add(target string, callback DnsMonitorCallback) (*DnsMonito
|
|||
return e, nil
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Remove(entry *DnsMonitorEntry) {
|
||||
func (m *Monitor) Remove(entry *MonitorEntry) {
|
||||
oldEntry := entry.entry.Swap(nil)
|
||||
if oldEntry == nil {
|
||||
// Already removed.
|
||||
|
|
@ -245,7 +267,7 @@ func (m *DnsMonitor) Remove(entry *DnsMonitorEntry) {
|
|||
m.hasRemoved.Store(true)
|
||||
return
|
||||
}
|
||||
defer m.mu.Unlock()
|
||||
defer m.mu.Unlock() // +checklocksforce: only executed if the TryLock above succeeded.
|
||||
|
||||
e, found := m.hostnames[oldEntry.hostname]
|
||||
if !found {
|
||||
|
|
@ -257,7 +279,7 @@ func (m *DnsMonitor) Remove(entry *DnsMonitorEntry) {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) clearRemoved() {
|
||||
func (m *Monitor) clearRemoved() {
|
||||
if !m.hasRemoved.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
|
|
@ -266,21 +288,13 @@ func (m *DnsMonitor) clearRemoved() {
|
|||
defer m.mu.Unlock()
|
||||
|
||||
for hostname, entry := range m.hostnames {
|
||||
deleted := false
|
||||
for e := range entry.entries {
|
||||
if e.entry.Load() == nil {
|
||||
delete(entry.entries, e)
|
||||
deleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if deleted && len(entry.entries) == 0 {
|
||||
if entry.clearRemoved() {
|
||||
delete(m.hostnames, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) waitForEntries() (waited bool) {
|
||||
func (m *Monitor) waitForEntries() (waited bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
|
|
@ -291,7 +305,7 @@ func (m *DnsMonitor) waitForEntries() (waited bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) run() {
|
||||
func (m *Monitor) run() {
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
defer close(m.stopped)
|
||||
|
|
@ -302,21 +316,22 @@ func (m *DnsMonitor) run() {
|
|||
if m.stopCtx.Err() == nil {
|
||||
// Initial check when a new entry was added. More checks will be
|
||||
// triggered by the Ticker.
|
||||
m.checkHostnames()
|
||||
m.CheckHostnames()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
m.tickerWaiting.Store(true)
|
||||
select {
|
||||
case <-m.stopCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.checkHostnames()
|
||||
m.CheckHostnames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) doCheckHostnames() {
|
||||
func (m *Monitor) CheckHostnames() {
|
||||
m.clearRemoved()
|
||||
|
||||
m.mu.RLock()
|
||||
|
|
@ -327,17 +342,27 @@ func (m *DnsMonitor) doCheckHostnames() {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) checkHostname(entry *dnsMonitorEntry) {
|
||||
func (m *Monitor) checkHostname(entry *monitorEntry) {
|
||||
if len(entry.hostIP) > 0 {
|
||||
entry.setIPs([]net.IP{entry.hostIP}, true)
|
||||
return
|
||||
}
|
||||
|
||||
ips, err := lookupDnsMonitorIP(entry.hostname)
|
||||
ips, err := m.lookupFunc(entry.hostname)
|
||||
if err != nil {
|
||||
log.Printf("Could not lookup %s: %s", entry.hostname, err)
|
||||
m.logger.Printf("Could not lookup %s: %s", entry.hostname, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.setIPs(ips, false)
|
||||
}
|
||||
|
||||
func (m *Monitor) WaitForTicker(ctx context.Context) error {
|
||||
for !m.tickerWaiting.Load() {
|
||||
time.Sleep(time.Millisecond)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -33,61 +33,20 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/dns/internal"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
)
|
||||
|
||||
type mockDnsLookup struct {
|
||||
sync.RWMutex
|
||||
|
||||
ips map[string][]net.IP
|
||||
}
|
||||
|
||||
func newMockDnsLookupForTest(t *testing.T) *mockDnsLookup {
|
||||
mock := &mockDnsLookup{
|
||||
ips: make(map[string][]net.IP),
|
||||
}
|
||||
prev := lookupDnsMonitorIP
|
||||
t.Cleanup(func() {
|
||||
lookupDnsMonitorIP = prev
|
||||
})
|
||||
lookupDnsMonitorIP = mock.lookup
|
||||
return mock
|
||||
}
|
||||
|
||||
func (m *mockDnsLookup) Set(host string, ips []net.IP) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
m.ips[host] = ips
|
||||
}
|
||||
|
||||
func (m *mockDnsLookup) Get(host string) []net.IP {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.ips[host]
|
||||
}
|
||||
|
||||
func (m *mockDnsLookup) lookup(host string) ([]net.IP, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
ips, found := m.ips[host]
|
||||
if !found {
|
||||
return nil, &net.DNSError{
|
||||
Err: fmt.Sprintf("could not resolve %s", host),
|
||||
Name: host,
|
||||
IsNotFound: true,
|
||||
}
|
||||
}
|
||||
|
||||
return append([]net.IP{}, ips...), nil
|
||||
}
|
||||
|
||||
func newDnsMonitorForTest(t *testing.T, interval time.Duration) *DnsMonitor {
|
||||
func NewMonitorForTest(t *testing.T, interval time.Duration, lookup *internal.MockLookup) *Monitor {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
monitor, err := NewDnsMonitor(interval)
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
var lookupFunc MonitorLookupFunc
|
||||
if lookup != nil {
|
||||
lookupFunc = lookup.Lookup
|
||||
}
|
||||
monitor, err := NewMonitor(logger, interval, lookupFunc)
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
|
|
@ -98,46 +57,48 @@ func newDnsMonitorForTest(t *testing.T, interval time.Duration) *DnsMonitor {
|
|||
return monitor
|
||||
}
|
||||
|
||||
type dnsMonitorReceiverRecord struct {
|
||||
type monitorReceiverRecord struct {
|
||||
all []net.IP
|
||||
add []net.IP
|
||||
keep []net.IP
|
||||
remove []net.IP
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiverRecord) Equal(other *dnsMonitorReceiverRecord) bool {
|
||||
func (r *monitorReceiverRecord) Equal(other *monitorReceiverRecord) bool {
|
||||
return r == other || (reflect.DeepEqual(r.add, other.add) &&
|
||||
reflect.DeepEqual(r.keep, other.keep) &&
|
||||
reflect.DeepEqual(r.remove, other.remove))
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiverRecord) String() string {
|
||||
func (r *monitorReceiverRecord) String() string {
|
||||
return fmt.Sprintf("all=%v, add=%v, keep=%v, remove=%v", r.all, r.add, r.keep, r.remove)
|
||||
}
|
||||
|
||||
var (
|
||||
expectNone = &dnsMonitorReceiverRecord{}
|
||||
expectNone = &monitorReceiverRecord{} // +checklocksignore: Global readonly variable.
|
||||
)
|
||||
|
||||
type dnsMonitorReceiver struct {
|
||||
type monitorReceiver struct {
|
||||
sync.Mutex
|
||||
|
||||
t *testing.T
|
||||
expected *dnsMonitorReceiverRecord
|
||||
received *dnsMonitorReceiverRecord
|
||||
t *testing.T
|
||||
// +checklocks:Mutex
|
||||
expected *monitorReceiverRecord
|
||||
// +checklocks:Mutex
|
||||
received *monitorReceiverRecord
|
||||
}
|
||||
|
||||
func newDnsMonitorReceiverForTest(t *testing.T) *dnsMonitorReceiver {
|
||||
return &dnsMonitorReceiver{
|
||||
func newMonitorReceiverForTest(t *testing.T) *monitorReceiver {
|
||||
return &monitorReceiver{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all, add, keep, remove []net.IP) {
|
||||
func (r *monitorReceiver) OnLookup(entry *MonitorEntry, all, add, keep, remove []net.IP) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
received := &dnsMonitorReceiverRecord{
|
||||
received := &monitorReceiverRecord{
|
||||
all: all,
|
||||
add: add,
|
||||
keep: keep,
|
||||
|
|
@ -147,13 +108,13 @@ func (r *dnsMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all, add, keep, re
|
|||
expected := r.expected
|
||||
r.expected = nil
|
||||
if expected == expectNone {
|
||||
assert.Fail(r.t, "expected no event, got %v", received)
|
||||
assert.Fail(r.t, "expected no event", "received %v", received)
|
||||
return
|
||||
}
|
||||
|
||||
if expected == nil {
|
||||
if r.received != nil && !r.received.Equal(received) {
|
||||
assert.Fail(r.t, "already received %v, got %v", r.received, received)
|
||||
assert.Fail(r.t, "unexpected message", "already received %v, got %v", r.received, received)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -163,7 +124,7 @@ func (r *dnsMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all, add, keep, re
|
|||
r.expected = nil
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) WaitForExpected(ctx context.Context) {
|
||||
func (r *monitorReceiver) WaitForExpected(ctx context.Context) {
|
||||
r.t.Helper()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
|
@ -182,16 +143,16 @@ func (r *dnsMonitorReceiver) WaitForExpected(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) Expect(all, add, keep, remove []net.IP) {
|
||||
func (r *monitorReceiver) Expect(all, add, keep, remove []net.IP) {
|
||||
r.t.Helper()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.expected != nil && r.expected != expectNone {
|
||||
assert.Fail(r.t, "didn't get previously expected %v", r.expected)
|
||||
assert.Fail(r.t, "didn't get previous message", "expected %v", r.expected)
|
||||
}
|
||||
|
||||
expected := &dnsMonitorReceiverRecord{
|
||||
expected := &monitorReceiverRecord{
|
||||
all: all,
|
||||
add: add,
|
||||
keep: keep,
|
||||
|
|
@ -205,25 +166,26 @@ func (r *dnsMonitorReceiver) Expect(all, add, keep, remove []net.IP) {
|
|||
r.expected = expected
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) ExpectNone() {
|
||||
func (r *monitorReceiver) ExpectNone() {
|
||||
r.t.Helper()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.expected != nil && r.expected != expectNone {
|
||||
assert.Fail(r.t, "didn't get previously expected %v", r.expected)
|
||||
assert.Fail(r.t, "didn't get previous message", "expected %v", r.expected)
|
||||
}
|
||||
|
||||
r.expected = expectNone
|
||||
}
|
||||
|
||||
func TestDnsMonitor(t *testing.T) {
|
||||
lookup := newMockDnsLookupForTest(t)
|
||||
func TestMonitor(t *testing.T) {
|
||||
t.Parallel()
|
||||
lookup := internal.NewMockLookup()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
monitor := NewMonitorForTest(t, interval, lookup)
|
||||
|
||||
ip1 := net.ParseIP("192.168.0.1")
|
||||
ip2 := net.ParseIP("192.168.1.1")
|
||||
|
|
@ -234,7 +196,7 @@ func TestDnsMonitor(t *testing.T) {
|
|||
}
|
||||
lookup.Set("foo", ips1)
|
||||
|
||||
rec1 := newDnsMonitorReceiverForTest(t)
|
||||
rec1 := newMonitorReceiverForTest(t)
|
||||
rec1.Expect(ips1, ips1, nil, nil)
|
||||
|
||||
entry1, err := monitor.Add("https://foo:12345", rec1.OnLookup)
|
||||
|
|
@ -285,19 +247,20 @@ func TestDnsMonitor(t *testing.T) {
|
|||
time.Sleep(5 * interval)
|
||||
}
|
||||
|
||||
func TestDnsMonitorIP(t *testing.T) {
|
||||
func TestMonitorIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
monitor := NewMonitorForTest(t, interval, nil)
|
||||
|
||||
ip := "192.168.0.1"
|
||||
ips := []net.IP{
|
||||
net.ParseIP(ip),
|
||||
}
|
||||
|
||||
rec1 := newDnsMonitorReceiverForTest(t)
|
||||
rec1 := newMonitorReceiverForTest(t)
|
||||
rec1.Expect(ips, ips, nil, nil)
|
||||
|
||||
entry, err := monitor.Add(ip+":12345", rec1.OnLookup)
|
||||
|
|
@ -310,14 +273,15 @@ func TestDnsMonitorIP(t *testing.T) {
|
|||
time.Sleep(5 * interval)
|
||||
}
|
||||
|
||||
func TestDnsMonitorNoLookupIfEmpty(t *testing.T) {
|
||||
func TestMonitorNoLookupIfEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
monitor := NewMonitorForTest(t, interval, nil)
|
||||
|
||||
var checked atomic.Bool
|
||||
monitor.checkHostnames = func() {
|
||||
monitor.lookupFunc = func(hostname string) ([]net.IP, error) {
|
||||
checked.Store(true)
|
||||
monitor.doCheckHostnames()
|
||||
return net.LookupIP(hostname)
|
||||
}
|
||||
|
||||
time.Sleep(10 * interval)
|
||||
|
|
@ -326,18 +290,20 @@ func TestDnsMonitorNoLookupIfEmpty(t *testing.T) {
|
|||
|
||||
type deadlockMonitorReceiver struct {
|
||||
t *testing.T
|
||||
monitor *DnsMonitor
|
||||
monitor *Monitor // +checklocksignore: Only written to from constructor.
|
||||
|
||||
mu sync.RWMutex
|
||||
wg sync.WaitGroup
|
||||
wg sync.WaitGroup // +checklocksignore: Only written to from constructor.
|
||||
|
||||
entry *DnsMonitorEntry
|
||||
started chan struct{}
|
||||
// +checklocks:mu
|
||||
entry *MonitorEntry
|
||||
started chan struct{}
|
||||
// +checklocks:mu
|
||||
triggered bool
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func newDeadlockMonitorReceiver(t *testing.T, monitor *DnsMonitor) *deadlockMonitorReceiver {
|
||||
func newDeadlockMonitorReceiver(t *testing.T, monitor *Monitor) *deadlockMonitorReceiver {
|
||||
return &deadlockMonitorReceiver{
|
||||
t: t,
|
||||
monitor: monitor,
|
||||
|
|
@ -345,7 +311,7 @@ func newDeadlockMonitorReceiver(t *testing.T, monitor *DnsMonitor) *deadlockMoni
|
|||
}
|
||||
}
|
||||
|
||||
func (r *deadlockMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
||||
func (r *deadlockMonitorReceiver) OnLookup(entry *MonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
||||
if !assert.False(r.t, r.closed.Load(), "received lookup after closed") {
|
||||
return
|
||||
}
|
||||
|
|
@ -358,16 +324,13 @@ func (r *deadlockMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all []net.IP,
|
|||
}
|
||||
|
||||
r.triggered = true
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
r.wg.Go(func() {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
close(r.started)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func (r *deadlockMonitorReceiver) Start() {
|
||||
|
|
@ -393,14 +356,15 @@ func (r *deadlockMonitorReceiver) Close() {
|
|||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func TestDnsMonitorDeadlock(t *testing.T) {
|
||||
lookup := newMockDnsLookupForTest(t)
|
||||
func TestMonitorDeadlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
lookup := internal.NewMockLookup()
|
||||
ip1 := net.ParseIP("192.168.0.1")
|
||||
ip2 := net.ParseIP("192.168.0.2")
|
||||
lookup.Set("foo", []net.IP{ip1})
|
||||
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
monitor := NewMonitorForTest(t, interval, lookup)
|
||||
|
||||
r := newDeadlockMonitorReceiver(t, monitor)
|
||||
r.Start()
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2022 struktur AG
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
|
|
@ -19,44 +19,41 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package signaling
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/dns"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/dns/internal"
|
||||
logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test"
|
||||
)
|
||||
|
||||
func UpdateCertificateCheckIntervalForTest(t *testing.T, interval time.Duration) {
|
||||
type MockLookup = internal.MockLookup
|
||||
|
||||
func NewMockLookup() *MockLookup {
|
||||
return internal.NewMockLookup()
|
||||
}
|
||||
|
||||
func NewMonitorForTest(t *testing.T, interval time.Duration, lookup *MockLookup) *dns.Monitor {
|
||||
t.Helper()
|
||||
// Make sure test is not executed with "t.Parallel()"
|
||||
t.Setenv("PARALLEL_CHECK", "1")
|
||||
old := deduplicateWatchEvents.Load()
|
||||
require := require.New(t)
|
||||
|
||||
logger := logtest.NewLoggerForTest(t)
|
||||
var lookupFunc dns.MonitorLookupFunc
|
||||
if lookup != nil {
|
||||
lookupFunc = lookup.Lookup
|
||||
}
|
||||
monitor, err := dns.NewMonitor(logger, interval, lookupFunc)
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
deduplicateWatchEvents.Store(old)
|
||||
monitor.Stop()
|
||||
})
|
||||
|
||||
deduplicateWatchEvents.Store(int64(interval))
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) WaitForReload(ctx context.Context) error {
|
||||
counter := r.GetReloadCounter()
|
||||
for counter == r.GetReloadCounter() {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CertPoolReloader) WaitForReload(ctx context.Context) error {
|
||||
counter := r.GetReloadCounter()
|
||||
for counter == r.GetReloadCounter() {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
require.NoError(monitor.Start())
|
||||
return monitor
|
||||
}
|
||||
68
dns/test/dns_test.go
Normal file
68
dns/test/dns_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/v2/dns"
|
||||
)
|
||||
|
||||
func TestDnsMonitor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
lookup := NewMockLookup()
|
||||
ips := []net.IP{
|
||||
net.ParseIP("1.2.3.4"),
|
||||
}
|
||||
lookup.Set("domain.invalid", ips)
|
||||
monitor := NewMonitorForTest(t, time.Second, lookup)
|
||||
|
||||
called := make(chan struct{})
|
||||
callback1 := func(entry *dns.MonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
||||
defer func() {
|
||||
called <- struct{}{}
|
||||
}()
|
||||
|
||||
assert.Equal(ips, all)
|
||||
assert.Equal(ips, add)
|
||||
assert.Empty(keep)
|
||||
assert.Empty(remove)
|
||||
}
|
||||
|
||||
entry1, err := monitor.Add("domain.invalid", callback1)
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
monitor.Remove(entry1)
|
||||
})
|
||||
|
||||
<-called
|
||||
}
|
||||
|
|
@ -15,7 +15,11 @@ The running container can be configured through different environment variables:
|
|||
|
||||
- `CONFIG`: Optional name of configuration file to use.
|
||||
- `HTTP_LISTEN`: Address of HTTP listener.
|
||||
- `HTTP_READ_TIMEOUT`: HTTP socket read timeout in seconds.
|
||||
- `HTTP_WRITE_TIMEOUT`: HTTP socket write timeout in seconds.
|
||||
- `HTTPS_LISTEN`: Address of HTTPS listener.
|
||||
- `HTTPS_READ_TIMEOUT`: HTTPS socket read timeout in seconds.
|
||||
- `HTTPS_WRITE_TIMEOUT`: HTTPS socket write timeout in seconds.
|
||||
- `HTTPS_CERTIFICATE`: Name of certificate file for the HTTPS listener.
|
||||
- `HTTPS_KEY`: Name of private key file for the HTTPS listener.
|
||||
- `HASH_KEY`: Secret value used to generate checksums of sessions (32 or 64 bytes).
|
||||
|
|
@ -24,11 +28,15 @@ The running container can be configured through different environment variables:
|
|||
- `BACKENDS_ALLOWALL`: Allow all backends. Extremly insecure - use only for development!
|
||||
- `BACKENDS_ALLOWALL_SECRET`: Secret when `BACKENDS_ALLOWALL` is enabled.
|
||||
- `BACKENDS`: Space-separated list of backend ids.
|
||||
- `BACKEND_<ID>_URL`: Url of backend `ID` (where `ID` is the uppercase backend id).
|
||||
- `BACKENDS_TIMEOUT`: Timeout in seconds for requests to backends.
|
||||
- `CONNECTIONS_PER_HOST`: Maximum number of concurrent backend connections per host.
|
||||
- `BACKEND_<ID>_URLS`: Comma-separated list of urls of backend `ID` (where `ID` is the uppercase backend id).
|
||||
- `BACKEND_<ID>_URL`: Url of backend `ID` (where `ID` is the uppercase backend id, deprecated).
|
||||
- `BACKEND_<ID>_SHARED_SECRET`: Shared secret for backend `ID` (where `ID` is the uppercase backend id).
|
||||
- `BACKEND_<ID>_SESSION_LIMIT`: Optional session limit for backend `ID` (where `ID` is the uppercase backend id).
|
||||
- `BACKEND_<ID>_MAX_STREAM_BITRATE`: Optional maximum bitrate for audio/video streams in backend `ID` (where `ID` is the uppercase backend id).
|
||||
- `BACKEND_<ID>_MAX_SCREEN_BITRATE`: Optional maximum bitrate for screensharing streams in backend `ID` (where `ID` is the uppercase backend id).
|
||||
- `FEDERATION_TIMEOUT`: Timeout for requests to federation targets in seconds.
|
||||
- `NATS_URL`: Optional URL of NATS server.
|
||||
- `ETCD_ENDPOINTS`: Static list of etcd endpoints (if etcd should be used).
|
||||
- `ETCD_DISCOVERY_SRV`: Alternative domain to use for DNS SRV configuration of etcd endpoints (if etcd should be used).
|
||||
|
|
@ -41,12 +49,15 @@ The running container can be configured through different environment variables:
|
|||
- `USE_PROXY`: Set to `1` if proxy servers should be used as WebRTC backends.
|
||||
- `PROXY_TOKEN_ID`: Id of the token to use when connecting to proxy servers.
|
||||
- `PROXY_TOKEN_KEY`: Private key for the configured token id.
|
||||
- `PROXY_TIMEOUT`: Timeout in seconds for requests to the proxy server.
|
||||
- `PROXY_URLS`: Space-separated list of proxy URLs to connect to.
|
||||
- `PROXY_DNS_DISCOVERY`: Enable DNS discovery on hostnames of configured static URLs.
|
||||
- `PROXY_ETCD`: Set to `1` if etcd should be used to configure proxy connections.
|
||||
- `PROXY_KEY_PREFIX`: Key prefix of proxy entries.
|
||||
- `MAX_STREAM_BITRATE`: Optional global maximum bitrate for audio/video streams.
|
||||
- `MAX_SCREEN_BITRATE`: Optional global maximum bitrate for screensharing streams.
|
||||
- `ALLOWED_CANDIDATES`: List of IP addresses / subnets that are allowed to be used by clients in candidates. The allowed list has preference over the blocked list below.
|
||||
- `BLOCKED_CANDIDATES`: List of IP addresses / subnets to filter from candidates received by clients.
|
||||
- `TURN_API_KEY`: API key that Janus will need to send when requesting TURN credentials.
|
||||
- `TURN_SECRET`: The shared secret to use for generating TURN credentials.
|
||||
- `TURN_SERVERS`: A comma-separated list of TURN servers to use.
|
||||
|
|
@ -109,6 +120,8 @@ The running container can be configured through different environment variables:
|
|||
- `JANUS_URL`: Url to Janus server.
|
||||
- `MAX_STREAM_BITRATE`: Optional maximum bitrate for audio/video streams.
|
||||
- `MAX_SCREEN_BITRATE`: Optional maximum bitrate for screensharing streams.
|
||||
- `ALLOWED_CANDIDATES`: List of IP addresses / subnets that are allowed to be used by clients in candidates. The allowed list has preference over the blocked list below.
|
||||
- `BLOCKED_CANDIDATES`: List of IP addresses / subnets to filter from candidates received by clients.
|
||||
- `STATS_IPS`: Comma-separated list of IP addresses that are allowed to access the stats endpoint.
|
||||
- `TRUSTED_PROXIES`: Comma-separated list of IPs / networks that are trusted proxies.
|
||||
- `ETCD_ENDPOINTS`: Static list of etcd endpoints (if etcd should be used).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder
|
||||
FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
|
|
@ -12,7 +12,8 @@ RUN touch /.dockerenv && \
|
|||
FROM alpine:3
|
||||
|
||||
ENV CONFIG=/config/proxy.conf
|
||||
RUN adduser -D spreedbackend && \
|
||||
RUN addgroup -g 850 spreedbackend && \
|
||||
adduser -D --uid 850 -S -H -G spreedbackend spreedbackend && \
|
||||
apk add --no-cache bash tzdata ca-certificates su-exec
|
||||
|
||||
COPY --from=builder /workdir/bin/proxy /usr/bin/nextcloud-spreed-signaling-proxy
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ if [ ! -f "$CONFIG" ]; then
|
|||
if [ -n "$MAX_SCREEN_BITRATE" ]; then
|
||||
sed -i "s|#maxscreenbitrate =.*|maxscreenbitrate = $MAX_SCREEN_BITRATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$ALLOWED_CANDIDATES" ]; then
|
||||
sed -i "s|#allowedcandidates =.*|allowedcandidates = $ALLOWED_CANDIDATES|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$BLOCKED_CANDIDATES" ]; then
|
||||
sed -i "s|#blockedcandidates =.*|blockedcandidates = $BLOCKED_CANDIDATES|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$TOKENS_ETCD" ]; then
|
||||
if [ -z "$HAS_ETCD" ]; then
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder
|
||||
FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
|
|
@ -12,7 +12,8 @@ RUN touch /.dockerenv && \
|
|||
FROM alpine:3
|
||||
|
||||
ENV CONFIG=/config/server.conf
|
||||
RUN adduser -D spreedbackend && \
|
||||
RUN addgroup -g 850 spreedbackend && \
|
||||
adduser -D --uid 850 -S -H -G spreedbackend spreedbackend && \
|
||||
apk add --no-cache bash tzdata ca-certificates su-exec
|
||||
|
||||
COPY --from=builder /workdir/bin/signaling /usr/bin/nextcloud-spreed-signaling
|
||||
|
|
|
|||
|
|
@ -39,8 +39,21 @@ if [ ! -f "$CONFIG" ]; then
|
|||
if [ -n "$HTTP_LISTEN" ]; then
|
||||
sed -i "s|#listen = 127.0.0.1:8080|listen = $HTTP_LISTEN|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$HTTP_READ_TIMEOUT" ]; then
|
||||
sed -i "/HTTP socket/,/HTTP socket/ s|#readtimeout =.*|readtimeout = $HTTP_READ_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$HTTP_WRITE_TIMEOUT" ]; then
|
||||
sed -i "/HTTP socket/,/HTTP socket/ s|#writetimeout =.*|writetimeout = $HTTP_WRITE_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$HTTPS_LISTEN" ]; then
|
||||
sed -i "s|#listen = 127.0.0.1:8443|listen = $HTTPS_LISTEN|" "$CONFIG"
|
||||
if [ -n "$HTTPS_READ_TIMEOUT" ]; then
|
||||
sed -i "/HTTPS socket/,/HTTPS socket/ s|#readtimeout =.*|readtimeout = $HTTPS_READ_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$HTTPS_WRITE_TIMEOUT" ]; then
|
||||
sed -i "/HTTPS socket/,/HTTPS socket/ s|#writetimeout =.*|writetimeout = $HTTPS_WRITE_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$HTTPS_CERTIFICATE" ]; then
|
||||
sed -i "s|certificate = /etc/nginx/ssl/server.crt|certificate = $HTTPS_CERTIFICATE|" "$CONFIG"
|
||||
|
|
@ -64,6 +77,9 @@ if [ ! -f "$CONFIG" ]; then
|
|||
else
|
||||
sed -i "s|#url = nats://localhost:4222|url = nats://loopback|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$FEDERATION_TIMEOUT" ]; then
|
||||
sed -i "/federation/,/federation/ s|#timeout =.*|timeout = $FEDERATION_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
|
||||
HAS_ETCD=
|
||||
if [ -n "$ETCD_ENDPOINTS" ]; then
|
||||
|
|
@ -103,6 +119,9 @@ if [ ! -f "$CONFIG" ]; then
|
|||
if [ -n "$PROXY_TOKEN_KEY" ]; then
|
||||
sed -i "s|#token_key =.*|token_key = $PROXY_TOKEN_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$PROXY_TIMEOUT" ]; then
|
||||
sed -i "s|#proxytimeout =.*|proxytimeout = $PROXY_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$PROXY_ETCD" ]; then
|
||||
if [ -z "$HAS_ETCD" ]; then
|
||||
|
|
@ -131,6 +150,12 @@ if [ ! -f "$CONFIG" ]; then
|
|||
if [ -n "$MAX_SCREEN_BITRATE" ]; then
|
||||
sed -i "s|#maxscreenbitrate =.*|maxscreenbitrate = $MAX_SCREEN_BITRATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$ALLOWED_CANDIDATES" ]; then
|
||||
sed -i "s|#allowedcandidates =.*|allowedcandidates = $ALLOWED_CANDIDATES|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$BLOCKED_CANDIDATES" ]; then
|
||||
sed -i "s|#blockedcandidates =.*|blockedcandidates = $BLOCKED_CANDIDATES|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$SKIP_VERIFY" ]; then
|
||||
sed -i "s|#skipverify =.*|skipverify = $SKIP_VERIFY|" "$CONFIG"
|
||||
|
|
@ -232,6 +257,14 @@ if [ ! -f "$CONFIG" ]; then
|
|||
sed -i "s|#secret = the-shared-secret-for-allowall|secret = $BACKENDS_ALLOWALL_SECRET|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$BACKENDS_TIMEOUT" ]; then
|
||||
sed -i "/requests to the backend/,/requests to the backend/ s|^timeout =.*|timeout = $BACKENDS_TIMEOUT|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$CONNECTIONS_PER_HOST" ]; then
|
||||
sed -i "s|connectionsperhost =.*|connectionsperhost = $CONNECTIONS_PER_HOST|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$BACKENDS" ]; then
|
||||
BACKENDS_CONFIG=${BACKENDS// /,}
|
||||
sed -i "s|#backends = .*|backends = $BACKENDS_CONFIG|" "$CONFIG"
|
||||
|
|
@ -240,9 +273,15 @@ if [ ! -f "$CONFIG" ]; then
|
|||
for backend in $BACKENDS; do
|
||||
echo "[$backend]" >> "$CONFIG"
|
||||
|
||||
declare var="BACKEND_${backend^^}_URL"
|
||||
declare var="BACKEND_${backend^^}_URLS"
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "url = ${!var}" >> "$CONFIG"
|
||||
echo "urls = ${!var}" >> "$CONFIG"
|
||||
else
|
||||
declare var_compat="BACKEND_${backend^^}_URL"
|
||||
if [ -n "${!var_compat}" ]; then
|
||||
echo "Variable $var_compat is deprecated, use $var instead."
|
||||
echo "urls = ${!var_compat}" >> "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
declare var="BACKEND_${backend^^}_SHARED_SECRET"
|
||||
|
|
@ -266,6 +305,8 @@ if [ ! -f "$CONFIG" ]; then
|
|||
fi
|
||||
echo >> "$CONFIG"
|
||||
done
|
||||
elif [ -n "$BACKENDS_COMPAT_ALLOWED" ]; then
|
||||
sed -i "s|#backends =.*|allowed = $BACKENDS_COMPAT_ALLOWED\nsecret = $BACKENDS_COMPAT_SECRET|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -52,3 +52,28 @@ The following metrics are available:
|
|||
| `signaling_http_client_pool_connections` | Gauge | 1.2.4 | The current number of HTTP client connections per host | `host` |
|
||||
| `signaling_throttle_delayed_total` | Counter | 1.2.5 | The total number of delayed requests | `action`, `delay` |
|
||||
| `signaling_throttle_bruteforce_total` | Counter | 1.2.5 | The total number of rejected bruteforce requests | `action` |
|
||||
| `signaling_backend_client_requests_total` | Counter | 2.0.3 | The total number of backend client requests | `backend` |
|
||||
| `signaling_backend_client_requests_duration` | Histogram | 2.0.3 | The duration of backend client requests in seconds | `backend` |
|
||||
| `signaling_backend_client_requests_errors_total` | Counter | 2.0.3 | The total number of backend client requests that had an error | `backend`, `error` |
|
||||
| `signaling_mcu_bandwidth` | Gauge | 2.1.0 | The current bandwidth in bytes per second | `direction` |
|
||||
| `signaling_mcu_backend_usage` | Gauge | 2.1.0 | The current usage of signaling proxy backends in percent | `url`, `direction` |
|
||||
| `signaling_mcu_backend_bandwidth` | Gauge | 2.1.0 | The current bandwidth of signaling proxy backends in bytes per second | `url`, `direction` |
|
||||
| `signaling_proxy_load` | Gauge | 2.1.0 | The current load of the signaling proxy | |
|
||||
| `signaling_client_rtt` | Histogram | 2.1.0 | The roundtrip time of WebSocket ping messages in milliseconds | |
|
||||
| `signaling_mcu_selected_candidate_total` | Counter | 2.1.0 | Total number of selected candidates | `origin`, `type`, `transport`, `family` |
|
||||
| `signaling_mcu_peerconnection_state_total` | Counter | 2.1.0 | Total number PeerConnection states | `state`, `reason` |
|
||||
| `signaling_mcu_ice_state_total` | Counter | 2.1.0 | Total number of ICE connection states | `state` |
|
||||
| `signaling_mcu_dtls_state_total` | Counter | 2.1.0 | Total number of DTLS connection states | `state` |
|
||||
| `signaling_mcu_slow_link_total` | Counter | 2.1.0 | Total number of slow link events | `media`, `direction` |
|
||||
| `signaling_mcu_media_rtt` | Histogram | 2.1.0 | The roundtrip time of WebRTC media in milliseconds | `media` |
|
||||
| `signaling_mcu_media_jitter` | Histogram | 2.1.0 | The jitter of WebRTC media in milliseconds | `media`, `origin` |
|
||||
| `signaling_mcu_media_codecs_total` | Counter | 2.1.0 | The total number of codecs | `media`, `codec` |
|
||||
| `signaling_mcu_media_nacks_total` | Counter | 2.1.0 | The total number of NACKs | `media`, `direction` |
|
||||
| `signaling_mcu_media_retransmissions_total` | Counter | 2.1.0 | The total number of received retransmissions | `media` |
|
||||
| `signaling_mcu_media_bytes_total` | Counter | 2.1.0 | The total number of media bytes sent / received | `media`, `direction` |
|
||||
| `signaling_mcu_media_lost_total` | Counter | 2.1.0 | The total number of lost media packets | `media`, `origin` |
|
||||
| `signaling_client_bytes_total` | Counter | 2.1.0 | The total number of bytes sent to or received by clients | `direction` |
|
||||
| `signaling_client_messages_total` | Counter | 2.1.0 | The total number of messages sent to or received by clients | `direction` |
|
||||
| `signaling_call_sessions` | Gauge | 2.1.0 | The current number of sessions in a call | `backend`, `room`, `clienttype` |
|
||||
| `signaling_call_sessions_total` | Counter | 2.1.0 | The total number of sessions in a call | `backend`, `clienttype` |
|
||||
| `signaling_call_rooms_total` | Counter | 2.1.0 | The total number of rooms with an active call | `backend` |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
jinja2==3.1.5
|
||||
markdown==3.7
|
||||
jinja2==3.1.6
|
||||
markdown==3.10.2
|
||||
mkdocs==1.6.1
|
||||
readthedocs-sphinx-search==0.3.2
|
||||
sphinx==8.1.3
|
||||
sphinx_rtd_theme==3.0.2
|
||||
sphinx==9.1.0
|
||||
sphinx_rtd_theme==3.1.0
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue