mirror of
https://github.com/strukturag/nextcloud-spreed-signaling
synced 2024-06-02 05:52:18 +02:00
Compare commits
922 commits
Author | SHA1 | Date | |
---|---|---|---|
fd77de0e02 | |||
c17882307f | |||
c7cccc9287 | |||
15edeca814 | |||
2a1fd2e018 | |||
be66d9425b | |||
b033e07e06 | |||
b47a112e7e | |||
6c62f9caae | |||
8b39217551 | |||
de3507690c | |||
8123be9551 | |||
cad442c486 | |||
e8ebfed711 | |||
8d8ec677f1 | |||
80d96916b9 | |||
8a0ce7c9b6 | |||
1952bfc2be | |||
b3d2f7b02c | |||
7583fb6486 | |||
040e663b37 | |||
15b1214413 | |||
05810e10ce | |||
7e7a04ad6c | |||
d25169d0ff | |||
79b76b1ca4 | |||
f8e37a1bca | |||
b5cbb917c5 | |||
e2ac08ae67 | |||
00d17bae97 | |||
ff69a294a9 | |||
5790e7a369 | |||
4c807c86e8 | |||
e862392872 | |||
39f4b2eb11 | |||
7f8e44b3b5 | |||
31b8c74d1c | |||
5f18913646 | |||
716a93538b | |||
2cd3418f09 | |||
c6cbe88d0e | |||
f73ad7b508 | |||
efb722a55e | |||
d63b1cf14a | |||
75060b25aa | |||
7e7a6d5c09 | |||
a4b8a81734 | |||
3ce963ee91 | |||
24c1a09662 | |||
56f5a72f61 | |||
a66c1d82bf | |||
d9deddfda7 | |||
9c99129242 | |||
63c42dd84c | |||
92cbc28065 | |||
132cf0d474 | |||
4fd929c15a | |||
879469df19 | |||
fe0a002adf | |||
7b555e91ec | |||
b2afa88bcc | |||
1bbc49351a | |||
dff78d0101 | |||
2ad2327090 | |||
4b76a49355 | |||
f6125dac3f | |||
c2e93cd92a | |||
4f8349d4c1 | |||
aac4874e72 | |||
936f83feb9 | |||
c1e9e02087 | |||
beee423a7c | |||
5a85fecb10 | |||
88575abea2 | |||
fdc43d12cd | |||
d03ea86991 | |||
18300ce89e | |||
d8f2f265ab | |||
ddbf1065f6 | |||
bad52af35a | |||
c58564c0e8 | |||
0b259a8171 | |||
3fc5f5253d | |||
3e92664edc | |||
0ee976d377 | |||
552474f6f0 | |||
09e010ee14 | |||
70a5318973 | |||
94a8f0f02b | |||
4603b2b290 | |||
a50d637107 | |||
307ffdc29a | |||
ec3ac62474 | |||
e3a163fbe5 | |||
cf36530b30 | |||
adc72aa578 | |||
ea0d31b0dc | |||
5b305f6f99 | |||
3c923a9ef9 | |||
1a692bc4bb | |||
6a495bfc5c | |||
9a91e885cf | |||
b4830b1fd3 | |||
16da87106a | |||
e763f4519c | |||
bfb185f382 | |||
46e8ea9148 | |||
4eb1b6609d | |||
815088f269 | |||
527061bbe2 | |||
a2f0bec564 | |||
70f0519ca2 | |||
9e2a896326 | |||
2d48018b58 | |||
cf19b3b1b4 | |||
ebb215c592 | |||
0eb234b24d | |||
cad397e59e | |||
f8899ef189 | |||
54c4f1847a | |||
d368a060fa | |||
602452fa25 | |||
0c2cefa63a | |||
2468443572 | |||
3721fb131f | |||
6960912681 | |||
b77525603c | |||
9adb762ccf | |||
bf68a15943 | |||
bc7aea68f3 | |||
69beea84cb | |||
952b8ae460 | |||
2e6cf7f86b | |||
dcec32be7e | |||
b0d052c6ec | |||
318ed3700f | |||
ee16a8d8be | |||
91033bf8c2 | |||
b541ebc4c6 | |||
0aed690463 | |||
71a4248568 | |||
df210a6a85 | |||
5bc9ada233 | |||
d0d68f0d21 | |||
9a892a194e | |||
26102e7acb | |||
88a575c36c | |||
fdab3db819 | |||
c8aa4c71e0 | |||
ec9e44f5d6 | |||
543a85f8aa | |||
9f104cb281 | |||
4e623a8e08 | |||
9ba5b4330a | |||
4b6a4dbfe1 | |||
e1f40a024e | |||
47fc6694ca | |||
d0c711b500 | |||
7dc450350b | |||
b2c6bd320b | |||
4f26d6e2a5 | |||
ddfd976627 | |||
879e1ca5b0 | |||
0b698556d6 | |||
ec96256f29 | |||
8d60f81969 | |||
280c2681be | |||
9c7b38d4ff | |||
283da1436a | |||
fdfeeefa39 | |||
f2bcc000ae | |||
2ef9b39959 | |||
1358285c4a | |||
68528d4674 | |||
cc7625c544 | |||
c325fbeae6 | |||
c859064a45 | |||
2f31532ee2 | |||
d97b071ccf | |||
95e2bc10d4 | |||
66dc55a3a5 | |||
74944ee547 | |||
61b8a91749 | |||
886ad912da | |||
3b4699c11e | |||
7844a9c21a | |||
f8eae0b71f | |||
0b7c17e083 | |||
c2eb3a8a27 | |||
010914eed9 | |||
1a93c42c38 | |||
3ba1853e5a | |||
bbdd991f05 | |||
b0f2e6ea33 | |||
f65bdf04ff | |||
1fa731f20e | |||
5dbee53a1b | |||
e6b3c8d24f | |||
687f4101c0 | |||
4fb7142a4e | |||
ec8cb8e1b8 | |||
2ee3fa509c | |||
204fec1583 | |||
42005d97c4 | |||
e2266a6765 | |||
9603ed3d6e | |||
9d313608cf | |||
84374590a4 | |||
a082874377 | |||
b67264e600 | |||
bd445bd99b | |||
df477a7856 | |||
1a8444ca71 | |||
bde0b08eb1 | |||
a68454ceec | |||
f6fe960534 | |||
fe53c32714 | |||
c3403b1e9a | |||
ba73d1a7df | |||
36e704e320 | |||
6394539876 | |||
2012a7a6df | |||
1bcf07afd3 | |||
3442cad9c3 | |||
edd042b00e | |||
8f4fc2db6d | |||
7d09c71ab9 | |||
26a65cedd1 | |||
9010e91ff4 | |||
da00080303 | |||
62b54a85ed | |||
3ea60cfe31 | |||
1f8b536c8a | |||
8385211fa2 | |||
f5007df0ad | |||
8b49cf8581 | |||
0f980f2894 | |||
6ac065f603 | |||
ae37a56e34 | |||
45be0ad2fd | |||
29b0b06f6d | |||
7e613f831b | |||
2e8b0dfe25 | |||
2348297f36 | |||
e0fe89f0f2 | |||
e0b3797ea9 | |||
35f9d313c7 | |||
68d4e87d31 | |||
27ebf9e037 | |||
f071a64797 | |||
6488ba1cf5 | |||
b710d1704e | |||
4a762a3264 | |||
55aee6e5dc | |||
2b62c9e3c1 | |||
1a0e51499f | |||
2430421006 | |||
5f71a9a0ab | |||
cf5ee8e4a1 | |||
c85b31bd24 | |||
5ec7fcb594 | |||
978024e799 | |||
d71ca35e97 | |||
48424bf290 | |||
a3ba73d764 | |||
9da78a1a8b | |||
fa0cb51c8e | |||
d0a3ce0616 | |||
2595420db1 | |||
32ccc2e50e | |||
0cd4099f7b | |||
c4fce20678 | |||
2c4cdedcae | |||
b1c78f6e9d | |||
8db4068989 | |||
528a09e5da | |||
8417f37cba | |||
7a6cffdc10 | |||
f5bef51917 | |||
390f288c1a | |||
11a89e0ca9 | |||
da4cf896c5 | |||
c89b7bbe8f | |||
20a34526c5 | |||
682134fe56 | |||
beaad80eba | |||
59cf86d786 | |||
a362682143 | |||
d3f41eb572 | |||
88e67cf95e | |||
cc25760dd6 | |||
530700e5af | |||
dbba13865d | |||
0a10339d17 | |||
02184ace70 | |||
07e2e25a07 | |||
2334f4815e | |||
47ad7619e0 | |||
6a384619b8 | |||
1d12b40867 | |||
a8c2a35221 | |||
dc3bcf2ce7 | |||
8c7882e4a6 | |||
67f20bd9b2 | |||
fea65b31dc | |||
d0085811f8 | |||
719bb9615d | |||
e3e302e453 | |||
3b509a5f43 | |||
0b61b8bb9f | |||
9b09ff083b | |||
682d3aa52a | |||
110ece7626 | |||
5fd0efa4bc | |||
bd9e2aa29d | |||
8f2933071e | |||
fb62c53976 | |||
2e0561b90b | |||
eeab0a226b | |||
32bbbeee32 | |||
7a8879051d | |||
075e50560e | |||
6c377ee173 | |||
eae19fd61a | |||
a751ede2b2 | |||
7d11ff4a41 | |||
833f39e608 | |||
4f5bdb2a3f | |||
116e74fab4 | |||
9c0e0ba85d | |||
23e6b11383 | |||
7e33d2cf3a | |||
7fe5995e1d | |||
2eb84a3301 | |||
2a16bf0650 | |||
d63856e263 | |||
734eaea85c | |||
e61845b086 | |||
0f83392e2d | |||
362098531b | |||
0d15971506 | |||
fd8d11806b | |||
a8180194ef | |||
dddf194b48 | |||
2f421e3bdf | |||
716be91feb | |||
b3dc84b7b8 | |||
55d143d6bc | |||
838e601183 | |||
4b019a991f | |||
a2faf3dc95 | |||
c20ff558f3 | |||
42a3a5a8a7 | |||
5d4cb58fc8 | |||
026dafe271 | |||
4aedfb0051 | |||
5ed1c94796 | |||
5fadace4f3 | |||
bbde039710 | |||
400e4883e4 | |||
d7c659295e | |||
a46cf6a504 | |||
7bedd963b0 | |||
57232b7e1f | |||
2bf877f561 | |||
7dfc4064e4 | |||
b1e0d231f7 | |||
6caae105c7 | |||
0a982dead3 | |||
97ae079cd6 | |||
2a40c89585 | |||
c72c821687 | |||
29d10f3723 | |||
00c2e4ea9f | |||
43b5243463 | |||
a43686ad92 | |||
c134883138 | |||
2c5ad32391 | |||
cc6b888830 | |||
ec7b178c4f | |||
20dbd95a44 | |||
29753a66b9 | |||
f4d84114e0 | |||
ceefe8dbfe | |||
c3bb9b45b9 | |||
e048da5f60 | |||
825d95c8d7 | |||
1fb2d93ce0 | |||
29998777a4 | |||
b1bd1a1f79 | |||
e972f911b0 | |||
4e140dd334 | |||
835836419e | |||
f448a09794 | |||
24193f47ac | |||
4868349ac8 | |||
213c836b9c | |||
a2a3771906 | |||
cd94e7886e | |||
9ef8cbe83e | |||
3f30271e72 | |||
0936d40f8b | |||
225f5bbd97 | |||
8e98ad3438 | |||
e333ddfd53 | |||
fdb4d74dd6 | |||
7c9632575b | |||
3a7b4c48dc | |||
e1273a3c52 | |||
d1544dcb2c | |||
04192e96f1 | |||
1333d821d9 | |||
861c4e628d | |||
0c552d7e16 | |||
39cd9f50fb | |||
8ebfa81c13 | |||
152bbe1287 | |||
4a9b25981d | |||
43f88c35a5 | |||
4bd656e8fd | |||
050475dfc8 | |||
d877f5193d | |||
4c0043521a | |||
da03095d1b | |||
c17c5fd444 | |||
e895fe3aeb | |||
b006903a56 | |||
1ace748432 | |||
4190f2709d | |||
a96a1c5ebc | |||
4d21091172 | |||
94ce9a6067 | |||
15f6204990 | |||
a232fb7fc3 | |||
b7be0bf910 | |||
858b176e37 | |||
5e82df1848 | |||
96fdf92d68 | |||
1905b81fb6 | |||
c7f3dc5853 | |||
3b4a8bc521 | |||
28ac875b90 | |||
366de99f30 | |||
6f60b23a33 | |||
342f54044c | |||
e3a8a6b1ac | |||
48b4e3beb8 | |||
4d7d5094bf | |||
4c87ecb4b3 | |||
9af73fa513 | |||
f62ada7f81 | |||
ac720bc2be | |||
7d12d74f38 | |||
0ab7c8efeb | |||
f355b25ffb | |||
35135433c2 | |||
92690f4613 | |||
bc7dba17c1 | |||
470ae9b703 | |||
ceea757a45 | |||
0f043a4ba9 | |||
46cec93d9f | |||
aeef825cd4 | |||
3e134f1278 | |||
ecdc5d75f0 | |||
26652fba1a | |||
a6b1998bba | |||
b0e9126848 | |||
9163bf435f | |||
543df327a8 | |||
84bde0591c | |||
2e6de510bb | |||
7914d4bc49 | |||
e427d0daa6 | |||
042a78f99d | |||
0591be1bad | |||
17e25bbe6e | |||
f514afad99 | |||
e9140178f9 | |||
e703982890 | |||
940365ea52 | |||
65209deb7d | |||
8a5dcb4ac9 | |||
0e55b5aa80 | |||
ebf9f3efc0 | |||
8bf0b53d0b | |||
5dc0dbe4d6 | |||
a275c62e9e | |||
8e17ea2ffb | |||
2b3f63ddcb | |||
e3600469c5 | |||
b758250aae | |||
d1771af3ff | |||
ae94c9f194 | |||
99b3bedb8b | |||
25d5933f1e | |||
3e7b57e005 | |||
12de5a9b71 | |||
6e81becd4c | |||
05fe7e8f7c | |||
5cba69686b | |||
31a57b65d5 | |||
03266eb323 | |||
694ef9a834 | |||
3473c48a67 | |||
cbd09f2bdf | |||
97abe3d016 | |||
332bb01b80 | |||
844def50df | |||
4a8f352304 | |||
ed0041898f | |||
b95c03babe | |||
0c3c245c37 | |||
4fa17018c8 | |||
18335071e9 | |||
1fafd16d36 | |||
6d492e3bfd | |||
fd29f83454 | |||
e83cdd67ad | |||
6da48e31b5 | |||
d22b4ef2ad | |||
4f5b5e3c83 | |||
4d92d14e0a | |||
7eb7f56f17 | |||
c0e60204ba | |||
58e5d4a18b | |||
e92d4ebe65 | |||
ab4cd5d838 | |||
c11902b2f3 | |||
b0f677dbc4 | |||
73808f5a68 | |||
a0c4919481 | |||
186c7bf052 | |||
ac74ced46c | |||
cd1a1aab0b | |||
29be44cb74 | |||
26b5ebb948 | |||
2175b372e2 | |||
bf488c4516 | |||
dc1c777f41 | |||
db0b591366 | |||
49ac751010 | |||
bd4f6524cb | |||
2fdd346766 | |||
e6c35a3354 | |||
2e2d2f64e9 | |||
3ed2a0e4cd | |||
c4ae9cdc6c | |||
b45f4a2bfc | |||
0b212819c7 | |||
a083cf3001 | |||
de9283969f | |||
9dad3f8aad | |||
84151b295a | |||
5a84dd7d65 | |||
40eea01644 | |||
0a343acacf | |||
514fd8e0fd | |||
3145cd3598 | |||
e0dd2617e7 | |||
df65394c12 | |||
b49797fc6c | |||
fc169a8f59 | |||
2c6b22640e | |||
e2c981c000 | |||
dd37df185e | |||
7d547f41d3 | |||
b6e83c7ffb | |||
433748e5e9 | |||
d4eafc851a | |||
fc61335b2e | |||
e4f7fc3020 | |||
5e0c7623be | |||
776a8080a7 | |||
be949f90b1 | |||
407fee2685 | |||
1e8dd56f7e | |||
91ab020c3f | |||
171262b068 | |||
8a83976d38 | |||
75347c553f | |||
d49147f376 | |||
2497e6585b | |||
31aa0e2a11 | |||
799e01df86 | |||
de701dbcae | |||
49a832f41a | |||
30a2fb134e | |||
1b4f1b09dd | |||
2174980c8a | |||
48bdf22f27 | |||
91307acee3 | |||
281bf73f62 | |||
74fe96bb50 | |||
9b27cf8bd1 | |||
c2182f0440 | |||
43ad9b794b | |||
93b9c08e90 | |||
149ea220a1 | |||
4f94d35eb1 | |||
107426f96d | |||
5730d806ec | |||
28c1f39062 | |||
db6fb9fc6b | |||
54fd71dae3 | |||
947782367d | |||
b9a6fad3fe | |||
066f03e5d6 | |||
af10d38c03 | |||
fa5c0bc637 | |||
31b69e97c2 | |||
ed5246b82d | |||
a52dde3594 | |||
75a9391b74 | |||
e32ede8717 | |||
6c6ebb647a | |||
4a7bf38bde | |||
84c8378fe8 | |||
42799f231b | |||
5921d5dcd3 | |||
e93291b100 | |||
63c51309ce | |||
4a43fe1df9 | |||
737d637987 | |||
3c6d2d1517 | |||
748f03cadc | |||
3d3297f006 | |||
d49d3704fa | |||
e9f80c6b4d | |||
20228b176f | |||
5e7dec014a | |||
8353cbbb0f | |||
e6b2d1e0aa | |||
2a47829164 | |||
758899b745 | |||
b17eb584b4 | |||
0b4e48af3b | |||
dc55e7d5c8 | |||
15490b802a | |||
5757b9ca30 | |||
a34f3b6093 | |||
cb68e074bb | |||
c8fa90d6ab | |||
f3ba485e9d | |||
f37b9b2fdd | |||
14876a92a8 | |||
2f6e2ba87c | |||
1997a8eecb | |||
02892abcf4 | |||
c281195575 | |||
6f5f069b80 | |||
9b4e369393 | |||
323b59f477 | |||
804d3d0b07 | |||
d56e67387b | |||
a0bb6a04a0 | |||
955a623419 | |||
e1761da4a8 | |||
2df1dc467a | |||
1bf860f9f1 | |||
570baa78f4 | |||
44fe19f9e3 | |||
bb24bf5f0d | |||
5266e58663 | |||
69dfb0686f | |||
1e1da6f8dd | |||
42b18d5547 | |||
7d4ba11207 | |||
7cf0bd8b88 | |||
335e280e62 | |||
313dfa2c61 | |||
50390ba1be | |||
1899628c1b | |||
067a69bc12 | |||
efe1ef368f | |||
982ad47e95 | |||
10fa5f03b1 | |||
b22d88df71 | |||
4943403cd7 | |||
5c6ac999a6 | |||
fa645ad2d3 | |||
1b13494c04 | |||
fdfd627b43 | |||
be39cee1ea | |||
a9b32ea833 | |||
86ee075a3b | |||
05b9f4d6c9 | |||
567183747a | |||
f5261135d2 | |||
b7f221705a | |||
6395b87577 | |||
89637c0a51 | |||
ef58f9087a | |||
8201e433d3 | |||
0020076f2b | |||
5a12959821 | |||
58bbe76b06 | |||
5af8636573 | |||
7bd6fdd93f | |||
8de8b39a5c | |||
2582e4ffb4 | |||
0f9090bced | |||
374492a3a8 | |||
f1f16f6a22 | |||
be4348fc3c | |||
e366def1ab | |||
a8ffcfa156 | |||
f6519a70c9 | |||
d8927601be | |||
8efb5c7f26 | |||
d7bbf2b0bd | |||
44a1a68db7 | |||
3226b32f28 | |||
5c9fdf8d4e | |||
7db6619a07 | |||
c1a6a6f586 | |||
bcd5180acb | |||
382898df53 | |||
cf9128c677 | |||
b4d957fbc5 | |||
b7e7170170 | |||
dd71201ed0 | |||
d6cf0ec534 | |||
3714de9822 | |||
00447dbc0d | |||
f425460fd9 | |||
dcfc73e81b | |||
b5fc698005 | |||
fb2f1519f0 | |||
6141847cec | |||
24d3a365b7 | |||
ea0d36290e | |||
0ef230a83f | |||
800a823b3d | |||
01c70b1182 | |||
e51d8127f5 | |||
fdb71f8858 | |||
34baa0113a | |||
e231ced83a | |||
70fcd848b5 | |||
324fe501b8 | |||
b68ced9483 | |||
a6becbd29f | |||
db03863346 | |||
f24340ede6 | |||
7ae4b79d02 | |||
7bce37f172 | |||
76a7a151d3 | |||
d72083068a | |||
fb8e1f0bd6 | |||
f2c74307d9 | |||
48a857bf6b | |||
bc8d2ee3ca | |||
374b8f935e | |||
a5e7dfe896 | |||
d8b051f6ff | |||
b65fe16002 | |||
e965f03f39 | |||
275fecbb5a | |||
fd49e627a9 | |||
9798c77673 | |||
ae26102447 | |||
64bdb36ce2 | |||
8cff75accf | |||
7f133e5998 | |||
c8ff009ecd | |||
ad5e9244ef | |||
a58f7ae118 | |||
10488a6352 | |||
b0b6f88a73 | |||
ea112f79eb | |||
9b20762fae | |||
5f716c3b64 | |||
19feb0c61f | |||
1bbc177196 | |||
9a2e9a2ce6 | |||
d8c58d87e0 | |||
af803cfbc7 | |||
a83c799eaf | |||
42a8aab307 | |||
1774d56d7d | |||
9a868c6d91 | |||
960cb0ea3c | |||
cdbc177179 | |||
4dfffca285 | |||
f1f018e338 | |||
06a1d072f1 | |||
708a513970 | |||
e01881a040 | |||
8fffe72c07 | |||
ba8b9f12a4 | |||
1ecf795f33 | |||
d2f4662232 | |||
ce1862bffc | |||
2cc0a3b2e5 | |||
bdd7d57ac6 | |||
f40fd33d9c | |||
c302f4f5a1 | |||
b2e8217c1f | |||
2394c09013 | |||
5843555f6f | |||
ff032ddec5 | |||
5f25c1c453 | |||
5b34d8cc3f | |||
Git'Fellow | 635c5ce9bd | ||
33dc5a554b | |||
b28a8b163b | |||
aaa9b2dde2 | |||
cbb6d9ca53 | |||
184c941f8a | |||
0338e9db42 | |||
9c159dc4f8 | |||
156bc360ff | |||
88e4c5f2c3 | |||
b14392b367 | |||
aa96695cc1 | |||
82e3b62c34 | |||
6e25490401 | |||
58d5a7c3d8 | |||
2d4ffcaeda | |||
04983e4b2c | |||
004252d767 | |||
fee5d22afb | |||
417a77aeae | |||
556ff5230d | |||
485991033d | |||
474987662a | |||
cded46e5cf | |||
75ae615d80 | |||
3ae23f0336 | |||
57b74e57a8 | |||
b478e875fe | |||
def519b9cf | |||
e40940c25b | |||
a9280f1b43 | |||
e54fcf9559 | |||
dcc583736a | |||
45c290a9fb | |||
51fb410c28 | |||
deaa17acc5 | |||
12a8fa98d0 | |||
706a876fc3 | |||
532587eb9f | |||
f0eb80b6fd | |||
faf7544f2d | |||
4f84d3ad0d | |||
7d9970713d | |||
e101e74672 | |||
6173a350a1 | |||
8ea6072de5 | |||
d2036fcbd6 | |||
d1c5d785c8 | |||
5fc61b15b6 | |||
13d2795b00 | |||
95d5e50705 | |||
3de149c7ae | |||
66e502dc9b | |||
4770fc8ad6 | |||
608415c3ff | |||
ab26dfe90d | |||
545bce0082 | |||
e99d843c65 | |||
a1f62ffd18 | |||
6e9a36a434 | |||
40e1b208c0 | |||
0165788fe3 | |||
8704bc3b5b | |||
75e5013dd8 | |||
5296e09a2e | |||
c463791e21 | |||
a9517feebb | |||
924fce6713 | |||
8a97fa7f5e | |||
ce5d74bbec | |||
5b3b147794 | |||
d3f8876d25 | |||
042d447ab4 | |||
243411671d | |||
f7db8a38e1 | |||
ad1dea2780 | |||
32a2f822e0 | |||
ec62503bd3 | |||
b2da4002a4 | |||
06e9ae0644 | |||
44bf8b74c2 | |||
15dabeee1e | |||
715b2317df | |||
24eab34da7 | |||
01858a89f4 | |||
20cc51c2fe | |||
5a242b2570 | |||
0e144906a4 | |||
dcb5be956c | |||
36710c8aa9 | |||
25dabf910d | |||
b6e419f18a | |||
b315c09a3b | |||
6f64ff901d | |||
2ca9fb21c4 | |||
a0d3af14e0 | |||
7b24dc1d1d | |||
ece2903413 | |||
0115c97946 | |||
ddb7ece622 | |||
a761f135a8 | |||
a06bc333d2 | |||
af4bd51ec0 | |||
b0624be0a9 | |||
134d22bfe7 | |||
28b94191b1 | |||
83ce95f39f | |||
79532954da | |||
3393ffde8a | |||
15a9bea122 | |||
f09c343592 | |||
da1efac59d | |||
4bedfdf780 | |||
078768f9c8 | |||
26f9edd476 | |||
31e7923ec1 | |||
6a1a00551d | |||
b83bf7cb5d |
|
@ -1,5 +1,3 @@
|
|||
/bin
|
||||
/docker/janus
|
||||
/Dockerfile
|
||||
/docker/*/Dockerfile
|
||||
/docker-compose.yml
|
||||
/vendor
|
||||
|
|
23
.github/dependabot.yml
vendored
23
.github/dependabot.yml
vendored
|
@ -1,14 +1,37 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docker/janus"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docker/proxy"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docker/server"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
etcd:
|
||||
patterns:
|
||||
- "go.etcd.io*"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
artifacts:
|
||||
patterns:
|
||||
- "actions/*-artifact"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/docs"
|
||||
|
|
17
.github/workflows/check-continentmap.yml
vendored
17
.github/workflows/check-continentmap.yml
vendored
|
@ -1,15 +1,30 @@
|
|||
name: check-continentmap
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/check-continentmap.yml'
|
||||
- 'scripts/get_continent_map.py'
|
||||
- 'Makefile'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/check-continentmap.yml'
|
||||
- 'scripts/get_continent_map.py'
|
||||
- 'Makefile'
|
||||
schedule:
|
||||
- cron: "0 2 * * SUN"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check continentmap
|
||||
run: make check-continentmap
|
||||
|
|
11
.github/workflows/codeql-analysis.yml
vendored
11
.github/workflows/codeql-analysis.yml
vendored
|
@ -16,6 +16,9 @@ on:
|
|||
schedule:
|
||||
- cron: '28 2 * * 5'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
@ -33,15 +36,15 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
13
.github/workflows/command-rebase.yml
vendored
13
.github/workflows/command-rebase.yml
vendored
|
@ -9,16 +9,21 @@ on:
|
|||
issue_comment:
|
||||
types: created
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: none
|
||||
|
||||
# On pull requests and if the comment starts with `/rebase`
|
||||
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase')
|
||||
|
||||
steps:
|
||||
- name: Add reaction on start
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
repository: ${{ github.event.repository.full_name }}
|
||||
|
@ -26,18 +31,18 @@ jobs:
|
|||
reaction-type: "+1"
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.7
|
||||
uses: cirrus-actions/rebase@1.8
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Add reaction on failure
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
if: failure()
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
|
158
.github/workflows/deploydocker.yml
vendored
Normal file
158
.github/workflows/deploydocker.yml
vendored
Normal file
|
@ -0,0 +1,158 @@
|
|||
name: Deploy to Docker Hub / GHCR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/deploydocker.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
- '*.conf.in'
|
||||
- 'docker/server/*'
|
||||
- 'docker/proxy/*'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
strukturag/nextcloud-spreed-signaling
|
||||
ghcr.io/strukturag/nextcloud-spreed-signaling
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha,prefix=
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
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
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
proxy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
strukturag/nextcloud-spreed-signaling
|
||||
ghcr.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.
|
||||
flavor: |
|
||||
suffix=-proxy,onlatest=true
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha,prefix=
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
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
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/proxy/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
27
.github/workflows/docker-compose.yml
vendored
27
.github/workflows/docker-compose.yml
vendored
|
@ -5,28 +5,43 @@ on:
|
|||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/docker-compose.yml'
|
||||
- 'docker-compose.yml'
|
||||
- '**/docker-compose.yml'
|
||||
- 'docker/server/Dockerfile'
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/docker-compose.yml'
|
||||
- 'docker-compose.yml'
|
||||
- '**/docker-compose.yml'
|
||||
- 'docker/server/Dockerfile'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pull:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
|
||||
- name: Pull Docker images
|
||||
run: docker-compose pull
|
||||
run: ./docker-compose -f docker/docker-compose.yml pull
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
|
||||
- name: Build Docker images
|
||||
run: docker-compose build
|
||||
run: ./docker-compose -f docker/docker-compose.yml build
|
||||
|
|
14
.github/workflows/docker-janus.yml
vendored
14
.github/workflows/docker-janus.yml
vendored
|
@ -12,17 +12,25 @@ on:
|
|||
- '.github/workflows/docker-janus.yml'
|
||||
- 'docker/janus/Dockerfile'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TEST_TAG: strukturag/nextcloud-spreed-signaling:janus-test
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: docker/janus
|
||||
load: true
|
||||
tags: ${{ env.TEST_TAG }}
|
||||
|
|
54
.github/workflows/docker.yml
vendored
54
.github/workflows/docker.yml
vendored
|
@ -3,20 +3,66 @@ name: Docker image
|
|||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/docker.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
- '*.conf.in'
|
||||
- 'docker/server/*'
|
||||
- 'docker/proxy/*'
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/docker.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
- '*.conf.in'
|
||||
- 'docker/server/*'
|
||||
- 'docker/proxy/*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TEST_TAG: strukturag/nextcloud-spreed-signaling:test
|
||||
|
||||
jobs:
|
||||
build:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
proxy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/proxy/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
46
.github/workflows/govuln.yml
vendored
Normal file
46
.github/workflows/govuln.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: Go Vulnerability Checker
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/govuln.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/govuln.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
schedule:
|
||||
- cron: "0 2 * * SUN"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.21"
|
||||
- "1.22"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- run: date
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -y update && sudo apt -y install protobuf-compiler
|
||||
make common
|
||||
|
||||
- name: Install and run govulncheck
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
46
.github/workflows/licensecheck.yml
vendored
Normal file
46
.github/workflows/licensecheck.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: licensecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/licensecheck.yml'
|
||||
- '**.go'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/licensecheck.yml'
|
||||
- '**.go'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
golang:
|
||||
name: golang
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install licensecheck
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install licensecheck
|
||||
|
||||
- id: licensecheck
|
||||
name: Check licenses
|
||||
run: |
|
||||
{
|
||||
echo 'CHECK_RESULT<<EOF'
|
||||
licensecheck *.go */*.go
|
||||
echo EOF
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Check for missing licenses
|
||||
run: |
|
||||
MISSING=$(echo "$CHECK_RESULT" | grep UNKNOWN || true)
|
||||
if [ -n "$MISSING" ]; then \
|
||||
echo "$MISSING"; \
|
||||
exit 1; \
|
||||
fi
|
53
.github/workflows/lint.yml
vendored
53
.github/workflows/lint.yml
vendored
|
@ -5,49 +5,58 @@ on:
|
|||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/lint.yml'
|
||||
- '.golangci.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/lint.yml'
|
||||
- '.golangci.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: golang
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.17"
|
||||
|
||||
- id: go-cache-paths
|
||||
run: |
|
||||
echo "::set-output name=go-build::$(go env GOCACHE)"
|
||||
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
|
||||
echo "::set-output name=go-version::$(go version | cut -d ' ' -f 3)"
|
||||
|
||||
- name: Go build cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-build }}
|
||||
key: ${{ runner.os }}-${{ steps.go-cache-paths.outputs.go-version }}-build-${{ hashFiles('**/go.mod', '**/go.sum') }}
|
||||
|
||||
- name: Go mod cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-mod }}
|
||||
key: ${{ runner.os }}-${{ steps.go-cache-paths.outputs.go-version }}-mod-${{ hashFiles('**/go.mod', '**/go.sum') }}
|
||||
go-version: "1.21"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -y update && sudo apt -y install protobuf-compiler
|
||||
make common
|
||||
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v3.2.0
|
||||
uses: golangci/golangci-lint-action@v6.0.1
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=2m0s
|
||||
skip-cache: true
|
||||
|
||||
dependencies:
|
||||
name: dependencies
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "stable"
|
||||
|
||||
- name: Check minimum supported version of Go
|
||||
run: |
|
||||
go mod tidy -go=1.21 -compat=1.21
|
||||
|
||||
- name: Check go.mod / go.sum
|
||||
run: |
|
||||
git add go.*
|
||||
git diff --cached --exit-code go.*
|
||||
|
|
27
.github/workflows/shellcheck.yml
vendored
Normal file
27
.github/workflows/shellcheck.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: shellcheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/shellcheck.yml'
|
||||
- '**.sh'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/shellcheck.yml'
|
||||
- '**.sh'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: shellcheck
|
||||
run: |
|
||||
find -name "*.sh" | xargs shellcheck
|
91
.github/workflows/tarball.yml
vendored
Normal file
91
.github/workflows/tarball.yml
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
name: tarball
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/tarball.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/tarball.yml'
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- 'Makefile'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
create:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.21"
|
||||
- "1.22"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -y update && sudo apt -y install protobuf-compiler
|
||||
|
||||
- name: Create tarball
|
||||
run: |
|
||||
echo "Building with $(nproc) threads"
|
||||
make tarball
|
||||
|
||||
- name: Upload tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tarball-${{ matrix.go-version }}
|
||||
path: nextcloud-spreed-signaling*.tar.gz
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.21"
|
||||
- "1.22"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create]
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -y update && sudo apt -y install protobuf-compiler
|
||||
|
||||
- name: Download tarball
|
||||
uses: actions/download-artifact@v4
|
||||
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
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOPROXY: off
|
||||
run: |
|
||||
echo "Building with $(nproc) threads"
|
||||
make -C tmp build -j$(nproc)
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
GOPROXY: off
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
run: |
|
||||
make -C tmp test TIMEOUT=120s
|
40
.github/workflows/test.yml
vendored
40
.github/workflows/test.yml
vendored
|
@ -16,39 +16,29 @@ on:
|
|||
- 'go.*'
|
||||
- 'Makefile'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
go:
|
||||
env:
|
||||
MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }}
|
||||
USE_DB_IP_GEOIP_DATABASE: "1"
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.17"
|
||||
- "1.18"
|
||||
- "1.21"
|
||||
- "1.22"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- id: go-cache-paths
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "::set-output name=go-build::$(go env GOCACHE)"
|
||||
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
|
||||
echo "::set-output name=go-version::$(go version | cut -d ' ' -f 3)"
|
||||
|
||||
- name: Go build cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-build }}
|
||||
key: ${{ runner.os }}-${{ steps.go-cache-paths.outputs.go-version }}-build-${{ hashFiles('**/go.mod', '**/go.sum') }}
|
||||
|
||||
- name: Go mod cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-mod }}
|
||||
key: ${{ runner.os }}-${{ steps.go-cache-paths.outputs.go-version }}-mod-${{ hashFiles('**/go.mod', '**/go.sum') }}
|
||||
sudo apt -y update && sudo apt -y install protobuf-compiler
|
||||
|
||||
- name: Build applications
|
||||
run: |
|
||||
|
@ -59,11 +49,11 @@ jobs:
|
|||
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test || make test
|
||||
make test TIMEOUT=120s
|
||||
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
make cover || make cover
|
||||
make cover TIMEOUT=120s
|
||||
echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV
|
||||
|
||||
- name: Convert coverage to lcov
|
||||
|
@ -73,7 +63,7 @@ jobs:
|
|||
outfile: cover.lcov
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@1.1.3
|
||||
uses: coverallsapp/github-action@v2.3.0
|
||||
env:
|
||||
COVERALLS_FLAG_NAME: run-${{ matrix.go-version }}
|
||||
with:
|
||||
|
@ -82,11 +72,13 @@ jobs:
|
|||
parallel: true
|
||||
|
||||
finish:
|
||||
permissions:
|
||||
contents: none
|
||||
needs: go
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@1.1.3
|
||||
uses: coverallsapp/github-action@v2.3.0
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel-finished: true
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@ vendor/
|
|||
|
||||
*_easyjson.go
|
||||
*.pem
|
||||
*.pb.go
|
||||
*.prof
|
||||
*.socket
|
||||
*.tar.gz
|
||||
|
|
|
@ -25,7 +25,7 @@ linters-settings:
|
|||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
#- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
|
|
782
CHANGELOG.md
782
CHANGELOG.md
|
@ -2,6 +2,788 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 1.3.1 - 2024-05-23
|
||||
|
||||
### Changed
|
||||
- Bump alpine from 3.19 to 3.20 in /docker/janus
|
||||
[#746](https://github.com/strukturag/nextcloud-spreed-signaling/pull/746)
|
||||
- CI: Remove deprecated options from lint workflow.
|
||||
[#748](https://github.com/strukturag/nextcloud-spreed-signaling/pull/748)
|
||||
- docker: Update Janus in example image to 1.2.2
|
||||
[#749](https://github.com/strukturag/nextcloud-spreed-signaling/pull/749)
|
||||
- Improve detection of actual client IP.
|
||||
[#747](https://github.com/strukturag/nextcloud-spreed-signaling/pull/747)
|
||||
|
||||
### Fixed
|
||||
- docker: Fix proxy entrypoint.
|
||||
[#745](https://github.com/strukturag/nextcloud-spreed-signaling/pull/745)
|
||||
|
||||
|
||||
## 1.3.0 - 2024-05-22
|
||||
|
||||
### Added
|
||||
- Support resuming remote sessions
|
||||
[#715](https://github.com/strukturag/nextcloud-spreed-signaling/pull/715)
|
||||
- Gracefully shut down signaling server on SIGUSR1.
|
||||
[#706](https://github.com/strukturag/nextcloud-spreed-signaling/pull/706)
|
||||
- docker: Add helper scripts to gracefully stop / wait for server.
|
||||
[#722](https://github.com/strukturag/nextcloud-spreed-signaling/pull/722)
|
||||
- Support environment variables in some configuration.
|
||||
[#721](https://github.com/strukturag/nextcloud-spreed-signaling/pull/721)
|
||||
- Add Context to clients / sessions.
|
||||
[#732](https://github.com/strukturag/nextcloud-spreed-signaling/pull/732)
|
||||
- Drop support for Golang 1.20
|
||||
[#737](https://github.com/strukturag/nextcloud-spreed-signaling/pull/737)
|
||||
- CI: Run "govulncheck".
|
||||
[#694](https://github.com/strukturag/nextcloud-spreed-signaling/pull/694)
|
||||
- Make trusted proxies configurable and default to loopback / private IPs.
|
||||
[#738](https://github.com/strukturag/nextcloud-spreed-signaling/pull/738)
|
||||
- Add support for remote streams (preview)
|
||||
[#708](https://github.com/strukturag/nextcloud-spreed-signaling/pull/708)
|
||||
- Add throttler for backend requests
|
||||
[#744](https://github.com/strukturag/nextcloud-spreed-signaling/pull/744)
|
||||
|
||||
### Changed
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.34.0 to 1.34.1
|
||||
[#697](https://github.com/strukturag/nextcloud-spreed-signaling/pull/697)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.62.1 to 1.63.0
|
||||
[#699](https://github.com/strukturag/nextcloud-spreed-signaling/pull/699)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.63.0 to 1.63.2
|
||||
[#700](https://github.com/strukturag/nextcloud-spreed-signaling/pull/700)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.12 to 2.10.14
|
||||
[#702](https://github.com/strukturag/nextcloud-spreed-signaling/pull/702)
|
||||
- Include previous value with etcd watch events.
|
||||
[#704](https://github.com/strukturag/nextcloud-spreed-signaling/pull/704)
|
||||
- build(deps): Bump go.uber.org/zap from 1.17.0 to 1.27.0
|
||||
[#705](https://github.com/strukturag/nextcloud-spreed-signaling/pull/705)
|
||||
- Improve support for Janus 1.x
|
||||
[#669](https://github.com/strukturag/nextcloud-spreed-signaling/pull/669)
|
||||
- build(deps): Bump sphinx from 7.2.6 to 7.3.5 in /docs
|
||||
[#709](https://github.com/strukturag/nextcloud-spreed-signaling/pull/709)
|
||||
- build(deps): Bump sphinx from 7.3.5 to 7.3.7 in /docs
|
||||
[#712](https://github.com/strukturag/nextcloud-spreed-signaling/pull/712)
|
||||
- build(deps): Bump golang.org/x/net from 0.21.0 to 0.23.0
|
||||
[#711](https://github.com/strukturag/nextcloud-spreed-signaling/pull/711)
|
||||
- Don't keep expiration timestamp in each session.
|
||||
[#713](https://github.com/strukturag/nextcloud-spreed-signaling/pull/713)
|
||||
- build(deps): Bump mkdocs from 1.5.3 to 1.6.0 in /docs
|
||||
[#714](https://github.com/strukturag/nextcloud-spreed-signaling/pull/714)
|
||||
- Speedup tests by running in parallel
|
||||
[#718](https://github.com/strukturag/nextcloud-spreed-signaling/pull/718)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 4.0.0 to 5.0.0
|
||||
[#719](https://github.com/strukturag/nextcloud-spreed-signaling/pull/719)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 5.0.0 to 5.1.0
|
||||
[#720](https://github.com/strukturag/nextcloud-spreed-signaling/pull/720)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.2.3 to 2.3.0
|
||||
[#728](https://github.com/strukturag/nextcloud-spreed-signaling/pull/728)
|
||||
- build(deps): Bump jinja2 from 3.1.3 to 3.1.4 in /docs
|
||||
[#726](https://github.com/strukturag/nextcloud-spreed-signaling/pull/726)
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.33.0 to 1.34.1
|
||||
[#725](https://github.com/strukturag/nextcloud-spreed-signaling/pull/725)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1
|
||||
[#730](https://github.com/strukturag/nextcloud-spreed-signaling/pull/730)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 5.1.0 to 6.0.1
|
||||
[#729](https://github.com/strukturag/nextcloud-spreed-signaling/pull/729)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.63.2 to 1.64.0
|
||||
[#734](https://github.com/strukturag/nextcloud-spreed-signaling/pull/734)
|
||||
- Validate received SDP earlier.
|
||||
[#707](https://github.com/strukturag/nextcloud-spreed-signaling/pull/707)
|
||||
- Log something if mcu publisher / subscriber was closed.
|
||||
[#736](https://github.com/strukturag/nextcloud-spreed-signaling/pull/736)
|
||||
- build(deps): Bump the etcd group with 4 updates
|
||||
[#693](https://github.com/strukturag/nextcloud-spreed-signaling/pull/693)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.34.1 to 1.35.0
|
||||
[#740](https://github.com/strukturag/nextcloud-spreed-signaling/pull/740)
|
||||
- Don't use unnecessary pointer to "json.RawMessage".
|
||||
[#739](https://github.com/strukturag/nextcloud-spreed-signaling/pull/739)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.14 to 2.10.15
|
||||
[#741](https://github.com/strukturag/nextcloud-spreed-signaling/pull/741)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.15 to 2.10.16
|
||||
[#743](https://github.com/strukturag/nextcloud-spreed-signaling/pull/743)
|
||||
|
||||
### Fixed
|
||||
- Improve detecting renames in file watcher.
|
||||
[#698](https://github.com/strukturag/nextcloud-spreed-signaling/pull/698)
|
||||
- Update etcd watch handling.
|
||||
[#701](https://github.com/strukturag/nextcloud-spreed-signaling/pull/701)
|
||||
- Prevent goroutine leaks in GRPC tests.
|
||||
[#716](https://github.com/strukturag/nextcloud-spreed-signaling/pull/716)
|
||||
- Fix potential race in capabilities test.
|
||||
[#731](https://github.com/strukturag/nextcloud-spreed-signaling/pull/731)
|
||||
- Don't log read error after we closed the connection.
|
||||
[#735](https://github.com/strukturag/nextcloud-spreed-signaling/pull/735)
|
||||
- Fix lock order inversion when leaving room / publishing room sessions.
|
||||
[#742](https://github.com/strukturag/nextcloud-spreed-signaling/pull/742)
|
||||
- Relax "MessageClientMessageData" validation.
|
||||
[#733](https://github.com/strukturag/nextcloud-spreed-signaling/pull/733)
|
||||
|
||||
|
||||
## 1.2.4 - 2024-04-03
|
||||
|
||||
### Added
|
||||
- Add metrics for current number of HTTP client connections.
|
||||
[#668](https://github.com/strukturag/nextcloud-spreed-signaling/pull/668)
|
||||
- Support getting GeoIP DB from db-ip.com for tests.
|
||||
[#689](https://github.com/strukturag/nextcloud-spreed-signaling/pull/689)
|
||||
- Use fsnotify to detect file changes
|
||||
[#680](https://github.com/strukturag/nextcloud-spreed-signaling/pull/680)
|
||||
- CI: Check dependencies for minimum supported version.
|
||||
[#692](https://github.com/strukturag/nextcloud-spreed-signaling/pull/692)
|
||||
|
||||
### Changed
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.9 to 2.10.10
|
||||
[#650](https://github.com/strukturag/nextcloud-spreed-signaling/pull/650)
|
||||
- CI: Also test with Golang 1.22
|
||||
[#651](https://github.com/strukturag/nextcloud-spreed-signaling/pull/651)
|
||||
- build(deps): Bump the etcd group with 4 updates
|
||||
[#649](https://github.com/strukturag/nextcloud-spreed-signaling/pull/649)
|
||||
- Improve Makefile
|
||||
[#653](https://github.com/strukturag/nextcloud-spreed-signaling/pull/653)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.61.0 to 1.61.1
|
||||
[#659](https://github.com/strukturag/nextcloud-spreed-signaling/pull/659)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 3.7.0 to 4.0.0
|
||||
[#658](https://github.com/strukturag/nextcloud-spreed-signaling/pull/658)
|
||||
- Minor improvements to DNS monitor
|
||||
[#663](https://github.com/strukturag/nextcloud-spreed-signaling/pull/663)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.10 to 2.10.11
|
||||
[#662](https://github.com/strukturag/nextcloud-spreed-signaling/pull/662)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.61.1 to 1.62.0
|
||||
[#664](https://github.com/strukturag/nextcloud-spreed-signaling/pull/664)
|
||||
- Support ports in full URLs for DNS monitor.
|
||||
[#667](https://github.com/strukturag/nextcloud-spreed-signaling/pull/667)
|
||||
- Calculate proxy load based on maximum bandwidth.
|
||||
[#670](https://github.com/strukturag/nextcloud-spreed-signaling/pull/670)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.32.0 to 1.33.1
|
||||
[#661](https://github.com/strukturag/nextcloud-spreed-signaling/pull/661)
|
||||
- build(deps): Bump golang from 1.21-alpine to 1.22-alpine in /docker/server
|
||||
[#655](https://github.com/strukturag/nextcloud-spreed-signaling/pull/655)
|
||||
- build(deps): Bump golang from 1.21-alpine to 1.22-alpine in /docker/proxy
|
||||
[#656](https://github.com/strukturag/nextcloud-spreed-signaling/pull/656)
|
||||
- docker: Update Janus from 0.11.8 to 0.14.1.
|
||||
[#672](https://github.com/strukturag/nextcloud-spreed-signaling/pull/672)
|
||||
- build(deps): Bump alpine from 3.18 to 3.19 in /docker/janus
|
||||
[#613](https://github.com/strukturag/nextcloud-spreed-signaling/pull/613)
|
||||
- Reuse backoff waiting code where possible
|
||||
[#673](https://github.com/strukturag/nextcloud-spreed-signaling/pull/673)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0
|
||||
[#674](https://github.com/strukturag/nextcloud-spreed-signaling/pull/674)
|
||||
- Docker improvements
|
||||
[#675](https://github.com/strukturag/nextcloud-spreed-signaling/pull/675)
|
||||
- make: Don't update dependencies but use pinned versions.
|
||||
[#679](https://github.com/strukturag/nextcloud-spreed-signaling/pull/679)
|
||||
- build(deps): Bump github.com/pion/sdp/v3 from 3.0.6 to 3.0.7
|
||||
[#678](https://github.com/strukturag/nextcloud-spreed-signaling/pull/678)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.62.0 to 1.62.1
|
||||
[#677](https://github.com/strukturag/nextcloud-spreed-signaling/pull/677)
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.32.0 to 1.33.0
|
||||
[#676](https://github.com/strukturag/nextcloud-spreed-signaling/pull/676)
|
||||
- build(deps): Bump github.com/pion/sdp/v3 from 3.0.7 to 3.0.8
|
||||
[#681](https://github.com/strukturag/nextcloud-spreed-signaling/pull/681)
|
||||
- Update source of continentmap to original CSV file.
|
||||
[#682](https://github.com/strukturag/nextcloud-spreed-signaling/pull/682)
|
||||
- build(deps): Bump markdown from 3.5.2 to 3.6 in /docs
|
||||
[#684](https://github.com/strukturag/nextcloud-spreed-signaling/pull/684)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.11 to 2.10.12
|
||||
[#683](https://github.com/strukturag/nextcloud-spreed-signaling/pull/683)
|
||||
- build(deps): Bump github.com/pion/sdp/v3 from 3.0.8 to 3.0.9
|
||||
[#687](https://github.com/strukturag/nextcloud-spreed-signaling/pull/687)
|
||||
- build(deps): Bump the etcd group with 4 updates
|
||||
[#686](https://github.com/strukturag/nextcloud-spreed-signaling/pull/686)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.33.1 to 1.34.0
|
||||
[#685](https://github.com/strukturag/nextcloud-spreed-signaling/pull/685)
|
||||
- Revert "build(deps): Bump the etcd group with 4 updates"
|
||||
[#691](https://github.com/strukturag/nextcloud-spreed-signaling/pull/691)
|
||||
- CI: Limit when to run Docker build jobs.
|
||||
[#695](https://github.com/strukturag/nextcloud-spreed-signaling/pull/695)
|
||||
- Remove deprecated section on multiple signaling servers from README.
|
||||
[#696](https://github.com/strukturag/nextcloud-spreed-signaling/pull/696)
|
||||
|
||||
### Fixed
|
||||
- Fix race condition when accessing "expected" in proxy_config tests.
|
||||
[#652](https://github.com/strukturag/nextcloud-spreed-signaling/pull/652)
|
||||
- Fix deadlock when entry is removed while receiver holds lock in lookup.
|
||||
[#654](https://github.com/strukturag/nextcloud-spreed-signaling/pull/654)
|
||||
- Fix flaky "TestProxyConfigStaticDNS".
|
||||
[#671](https://github.com/strukturag/nextcloud-spreed-signaling/pull/671)
|
||||
- Fix flaky DnsMonitor test.
|
||||
[#690](https://github.com/strukturag/nextcloud-spreed-signaling/pull/690)
|
||||
|
||||
|
||||
## 1.2.3 - 2024-01-31
|
||||
|
||||
### Added
|
||||
- CI: Check license headers.
|
||||
[#627](https://github.com/strukturag/nextcloud-spreed-signaling/pull/627)
|
||||
- Add "welcome" endpoint to proxy.
|
||||
[#644](https://github.com/strukturag/nextcloud-spreed-signaling/pull/644)
|
||||
|
||||
### Changed
|
||||
- build(deps): Bump github/codeql-action from 2 to 3
|
||||
[#619](https://github.com/strukturag/nextcloud-spreed-signaling/pull/619)
|
||||
- build(deps): Bump github.com/google/uuid from 1.4.0 to 1.5.0
|
||||
[#618](https://github.com/strukturag/nextcloud-spreed-signaling/pull/618)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.59.0 to 1.60.0
|
||||
[#617](https://github.com/strukturag/nextcloud-spreed-signaling/pull/617)
|
||||
- build(deps): Bump the artifacts group with 2 updates
|
||||
[#622](https://github.com/strukturag/nextcloud-spreed-signaling/pull/622)
|
||||
- build(deps): Bump golang.org/x/crypto from 0.16.0 to 0.17.0
|
||||
[#623](https://github.com/strukturag/nextcloud-spreed-signaling/pull/623)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.60.0 to 1.60.1
|
||||
[#624](https://github.com/strukturag/nextcloud-spreed-signaling/pull/624)
|
||||
- Refactor proxy config
|
||||
[#606](https://github.com/strukturag/nextcloud-spreed-signaling/pull/606)
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.31.0 to 1.32.0
|
||||
[#629](https://github.com/strukturag/nextcloud-spreed-signaling/pull/629)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0
|
||||
[#630](https://github.com/strukturag/nextcloud-spreed-signaling/pull/630)
|
||||
- build(deps): Bump jinja2 from 3.1.2 to 3.1.3 in /docs
|
||||
[#632](https://github.com/strukturag/nextcloud-spreed-signaling/pull/632)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.7 to 2.10.9
|
||||
[#633](https://github.com/strukturag/nextcloud-spreed-signaling/pull/633)
|
||||
- build(deps): Bump markdown from 3.5.1 to 3.5.2 in /docs
|
||||
[#631](https://github.com/strukturag/nextcloud-spreed-signaling/pull/631)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.31.0 to 1.32.0
|
||||
[#634](https://github.com/strukturag/nextcloud-spreed-signaling/pull/634)
|
||||
- build(deps): Bump readthedocs-sphinx-search from 0.3.1 to 0.3.2 in /docs
|
||||
[#635](https://github.com/strukturag/nextcloud-spreed-signaling/pull/635)
|
||||
- build(deps): Bump actions/cache from 3 to 4
|
||||
[#638](https://github.com/strukturag/nextcloud-spreed-signaling/pull/638)
|
||||
- build(deps): Bump github.com/google/uuid from 1.5.0 to 1.6.0
|
||||
[#643](https://github.com/strukturag/nextcloud-spreed-signaling/pull/643)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.60.1 to 1.61.0
|
||||
[#645](https://github.com/strukturag/nextcloud-spreed-signaling/pull/645)
|
||||
- build(deps): Bump peter-evans/create-or-update-comment from 3 to 4
|
||||
[#646](https://github.com/strukturag/nextcloud-spreed-signaling/pull/646)
|
||||
- CI: No longer need to manually cache Go modules.
|
||||
[#648](https://github.com/strukturag/nextcloud-spreed-signaling/pull/648)
|
||||
- CI: Disable cache for linter to bring back annotations.
|
||||
[#647](https://github.com/strukturag/nextcloud-spreed-signaling/pull/647)
|
||||
- Refactor DNS monitoring
|
||||
[#648](https://github.com/strukturag/nextcloud-spreed-signaling/pull/648)
|
||||
|
||||
### Fixed
|
||||
- Fix link to NATS install docs
|
||||
[#637](https://github.com/strukturag/nextcloud-spreed-signaling/pull/637)
|
||||
- docker: Always need to set proxy token id / key for server.
|
||||
[#641](https://github.com/strukturag/nextcloud-spreed-signaling/pull/641)
|
||||
|
||||
|
||||
## 1.2.2 - 2023-12-11
|
||||
|
||||
### Added
|
||||
- Include "~docker" in version if built on Docker.
|
||||
[#602](https://github.com/strukturag/nextcloud-spreed-signaling/pull/602)
|
||||
|
||||
### Changed
|
||||
- CI: No need to build docker images for testing, done internally.
|
||||
[#603](https://github.com/strukturag/nextcloud-spreed-signaling/pull/603)
|
||||
- build(deps): Bump sphinx-rtd-theme from 1.3.0 to 2.0.0 in /docs
|
||||
[#604](https://github.com/strukturag/nextcloud-spreed-signaling/pull/604)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.5 to 2.10.6
|
||||
[#605](https://github.com/strukturag/nextcloud-spreed-signaling/pull/605)
|
||||
- build(deps): Bump actions/setup-go from 4 to 5
|
||||
[#608](https://github.com/strukturag/nextcloud-spreed-signaling/pull/608)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.6 to 2.10.7
|
||||
[#612](https://github.com/strukturag/nextcloud-spreed-signaling/pull/612)
|
||||
- build(deps): Bump the etcd group with 4 updates
|
||||
[#611](https://github.com/strukturag/nextcloud-spreed-signaling/pull/611)
|
||||
|
||||
### Fixed
|
||||
- Skip options from default section when parsing "geoip-overrides".
|
||||
[#609](https://github.com/strukturag/nextcloud-spreed-signaling/pull/609)
|
||||
- Hangup virtual session if it gets disinvited.
|
||||
[#610](https://github.com/strukturag/nextcloud-spreed-signaling/pull/610)
|
||||
|
||||
|
||||
## 1.2.1 - 2023-11-15
|
||||
|
||||
### Added
|
||||
- feat(scripts): Add a script to simplify the logs to make it more easily to trace a user/session
|
||||
[#480](https://github.com/strukturag/nextcloud-spreed-signaling/pull/480)
|
||||
|
||||
### Changed
|
||||
- build(deps): Bump markdown from 3.5 to 3.5.1 in /docs
|
||||
[#594](https://github.com/strukturag/nextcloud-spreed-signaling/pull/594)
|
||||
- build(deps): Bump github.com/gorilla/websocket from 1.5.0 to 1.5.1
|
||||
[#595](https://github.com/strukturag/nextcloud-spreed-signaling/pull/595)
|
||||
- build(deps): Bump github.com/gorilla/securecookie from 1.1.1 to 1.1.2
|
||||
[#597](https://github.com/strukturag/nextcloud-spreed-signaling/pull/597)
|
||||
- build(deps): Bump github.com/gorilla/mux from 1.8.0 to 1.8.1
|
||||
[#596](https://github.com/strukturag/nextcloud-spreed-signaling/pull/596)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.4 to 2.10.5
|
||||
[#599](https://github.com/strukturag/nextcloud-spreed-signaling/pull/599)
|
||||
- Improve support for multiple backends with dialouts
|
||||
[#592](https://github.com/strukturag/nextcloud-spreed-signaling/pull/592)
|
||||
- build(deps): Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.25.0 to 0.46.0
|
||||
[#600](https://github.com/strukturag/nextcloud-spreed-signaling/pull/600)
|
||||
|
||||
|
||||
## 1.2.0 - 2023-10-30
|
||||
|
||||
### Added
|
||||
- Use GeoIP overrides if no GeoIP database is configured.
|
||||
[#532](https://github.com/strukturag/nextcloud-spreed-signaling/pull/532)
|
||||
- Log warning if no (static) backends have been configured.
|
||||
[#533](https://github.com/strukturag/nextcloud-spreed-signaling/pull/533)
|
||||
- Fallback to common shared secret if none is set for backends.
|
||||
[#534](https://github.com/strukturag/nextcloud-spreed-signaling/pull/534)
|
||||
- CI: Test with Golang 1.21
|
||||
[#536](https://github.com/strukturag/nextcloud-spreed-signaling/pull/536)
|
||||
- Return response if session tries to join room again.
|
||||
[#547](https://github.com/strukturag/nextcloud-spreed-signaling/pull/547)
|
||||
- Support TTL for transient data.
|
||||
[#575](https://github.com/strukturag/nextcloud-spreed-signaling/pull/575)
|
||||
- Implement message handler for dialout support.
|
||||
[#563](https://github.com/strukturag/nextcloud-spreed-signaling/pull/563)
|
||||
- No longer support Golang 1.19.
|
||||
[#580](https://github.com/strukturag/nextcloud-spreed-signaling/pull/580)
|
||||
|
||||
### Changed
|
||||
- build(deps): Bump google.golang.org/grpc from 1.56.1 to 1.57.0
|
||||
[#520](https://github.com/strukturag/nextcloud-spreed-signaling/pull/520)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.2.0 to 2.2.1
|
||||
[#514](https://github.com/strukturag/nextcloud-spreed-signaling/pull/514)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.27.1 to 1.28.0
|
||||
[#515](https://github.com/strukturag/nextcloud-spreed-signaling/pull/515)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.19 to 2.9.20
|
||||
[#513](https://github.com/strukturag/nextcloud-spreed-signaling/pull/513)
|
||||
- build(deps): Bump mkdocs from 1.4.3 to 1.5.1 in /docs
|
||||
[#523](https://github.com/strukturag/nextcloud-spreed-signaling/pull/523)
|
||||
- build(deps): Bump markdown from 3.3.7 to 3.4.4 in /docs
|
||||
[#519](https://github.com/strukturag/nextcloud-spreed-signaling/pull/519)
|
||||
- build(deps): Bump mkdocs from 1.5.1 to 1.5.2 in /docs
|
||||
[#525](https://github.com/strukturag/nextcloud-spreed-signaling/pull/525)
|
||||
- build(deps): Bump github.com/oschwald/maxminddb-golang from 1.11.0 to 1.12.0
|
||||
[#524](https://github.com/strukturag/nextcloud-spreed-signaling/pull/524)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.20 to 2.9.21
|
||||
[#530](https://github.com/strukturag/nextcloud-spreed-signaling/pull/530)
|
||||
- build(deps): Bump sphinx from 6.2.1 to 7.2.4 in /docs
|
||||
[#542](https://github.com/strukturag/nextcloud-spreed-signaling/pull/542)
|
||||
- build(deps): Bump github.com/google/uuid from 1.3.0 to 1.3.1
|
||||
[#539](https://github.com/strukturag/nextcloud-spreed-signaling/pull/539)
|
||||
- build(deps): Bump sphinx from 7.2.4 to 7.2.5 in /docs
|
||||
[#544](https://github.com/strukturag/nextcloud-spreed-signaling/pull/544)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.2.1 to 2.2.2
|
||||
[#546](https://github.com/strukturag/nextcloud-spreed-signaling/pull/546)
|
||||
- build(deps): Bump actions/checkout from 3 to 4
|
||||
[#545](https://github.com/strukturag/nextcloud-spreed-signaling/pull/545)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.57.0 to 1.58.0
|
||||
[#549](https://github.com/strukturag/nextcloud-spreed-signaling/pull/549)
|
||||
- build(deps): Bump docker/metadata-action from 4 to 5
|
||||
[#552](https://github.com/strukturag/nextcloud-spreed-signaling/pull/552)
|
||||
- build(deps): Bump docker/setup-qemu-action from 2 to 3
|
||||
[#553](https://github.com/strukturag/nextcloud-spreed-signaling/pull/553)
|
||||
- build(deps): Bump docker/login-action from 2 to 3
|
||||
[#554](https://github.com/strukturag/nextcloud-spreed-signaling/pull/554)
|
||||
- build(deps): Bump docker/setup-buildx-action from 2 to 3
|
||||
[#555](https://github.com/strukturag/nextcloud-spreed-signaling/pull/555)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.2.2 to 2.2.3
|
||||
[#551](https://github.com/strukturag/nextcloud-spreed-signaling/pull/551)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.21 to 2.9.22
|
||||
[#550](https://github.com/strukturag/nextcloud-spreed-signaling/pull/550)
|
||||
- build(deps): Bump docker/build-push-action from 4 to 5
|
||||
[#557](https://github.com/strukturag/nextcloud-spreed-signaling/pull/557)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.28.0 to 1.29.0
|
||||
[#558](https://github.com/strukturag/nextcloud-spreed-signaling/pull/558)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.58.0 to 1.58.1
|
||||
[#559](https://github.com/strukturag/nextcloud-spreed-signaling/pull/559)
|
||||
- build(deps): Bump sphinx from 7.2.5 to 7.2.6 in /docs
|
||||
[#560](https://github.com/strukturag/nextcloud-spreed-signaling/pull/560)
|
||||
- build(deps): Bump mkdocs from 1.5.2 to 1.5.3 in /docs
|
||||
[#561](https://github.com/strukturag/nextcloud-spreed-signaling/pull/561)
|
||||
- build(deps): Bump markdown from 3.4.4 to 3.5 in /docs
|
||||
[#570](https://github.com/strukturag/nextcloud-spreed-signaling/pull/570)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.58.1 to 1.58.3
|
||||
[#573](https://github.com/strukturag/nextcloud-spreed-signaling/pull/573)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0
|
||||
[#569](https://github.com/strukturag/nextcloud-spreed-signaling/pull/569)
|
||||
- build(deps): Bump golang.org/x/net from 0.12.0 to 0.17.0
|
||||
[#574](https://github.com/strukturag/nextcloud-spreed-signaling/pull/574)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.29.0 to 1.30.2
|
||||
[#568](https://github.com/strukturag/nextcloud-spreed-signaling/pull/568)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.58.3 to 1.59.0
|
||||
[#578](https://github.com/strukturag/nextcloud-spreed-signaling/pull/578)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.30.2 to 1.31.0
|
||||
[#577](https://github.com/strukturag/nextcloud-spreed-signaling/pull/577)
|
||||
- dependabot: Check for updates in docker files.
|
||||
- build(deps): Bump golang from 1.20-alpine to 1.21-alpine in /docker/proxy
|
||||
[#581](https://github.com/strukturag/nextcloud-spreed-signaling/pull/581)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.22 to 2.10.3
|
||||
[#576](https://github.com/strukturag/nextcloud-spreed-signaling/pull/576)
|
||||
- build(deps): Bump alpine from 3.14 to 3.18 in /docker/janus
|
||||
[#582](https://github.com/strukturag/nextcloud-spreed-signaling/pull/582)
|
||||
- build(deps): Bump golang from 1.20-alpine to 1.21-alpine in /docker/server
|
||||
[#583](https://github.com/strukturag/nextcloud-spreed-signaling/pull/583)
|
||||
- Improve get-version.sh
|
||||
[#584](https://github.com/strukturag/nextcloud-spreed-signaling/pull/584)
|
||||
-build(deps): Bump go.etcd.io/etcd/client/pkg/v3 from 3.5.9 to 3.5.10
|
||||
[#588](https://github.com/strukturag/nextcloud-spreed-signaling/pull/588)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.10.3 to 2.10.4
|
||||
[#586](https://github.com/strukturag/nextcloud-spreed-signaling/pull/586)
|
||||
- build(deps): Bump github.com/google/uuid from 1.3.1 to 1.4.0
|
||||
[#585](https://github.com/strukturag/nextcloud-spreed-signaling/pull/585)
|
||||
- dependabot: Group etcd updates.
|
||||
- build(deps): Bump the etcd group with 3 updates
|
||||
[#590](https://github.com/strukturag/nextcloud-spreed-signaling/pull/590)
|
||||
- Switch to atomic types from Go 1.19
|
||||
[#500](https://github.com/strukturag/nextcloud-spreed-signaling/pull/500)
|
||||
- Move common flags code to own struct.
|
||||
[#591](https://github.com/strukturag/nextcloud-spreed-signaling/pull/591)
|
||||
|
||||
|
||||
## 1.1.3 - 2023-07-05
|
||||
|
||||
### Added
|
||||
- stats: Support configuring subnets for allowed IPs.
|
||||
[#448](https://github.com/strukturag/nextcloud-spreed-signaling/pull/448)
|
||||
- Add common code to handle allowed IPs.
|
||||
[#450](https://github.com/strukturag/nextcloud-spreed-signaling/pull/450)
|
||||
- Add allowall to docker image
|
||||
[#488](https://github.com/strukturag/nextcloud-spreed-signaling/pull/488)
|
||||
- Follow the Go release policy by supporting only the last two versions.
|
||||
This drops support for Golang 1.18.
|
||||
[#499](https://github.com/strukturag/nextcloud-spreed-signaling/pull/499)
|
||||
|
||||
### Changed
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.29.0 to 1.29.1
|
||||
[#446](https://github.com/strukturag/nextcloud-spreed-signaling/pull/446)
|
||||
- build(deps): Bump actions/setup-go from 3 to 4
|
||||
[#447](https://github.com/strukturag/nextcloud-spreed-signaling/pull/447)
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.29.1 to 1.30.0
|
||||
[#449](https://github.com/strukturag/nextcloud-spreed-signaling/pull/449)
|
||||
- build(deps): Bump coverallsapp/github-action from 1.2.4 to 2.0.0
|
||||
[#451](https://github.com/strukturag/nextcloud-spreed-signaling/pull/451)
|
||||
- build(deps): Bump readthedocs-sphinx-search from 0.2.0 to 0.3.1 in /docs
|
||||
[#456](https://github.com/strukturag/nextcloud-spreed-signaling/pull/456)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.0.0 to 2.1.0
|
||||
[#460](https://github.com/strukturag/nextcloud-spreed-signaling/pull/460)
|
||||
- build(deps): Bump peter-evans/create-or-update-comment from 2 to 3
|
||||
[#459](https://github.com/strukturag/nextcloud-spreed-signaling/pull/459)
|
||||
- build(deps): Bump sphinx from 6.1.3 to 6.2.1 in /docs
|
||||
[#468](https://github.com/strukturag/nextcloud-spreed-signaling/pull/468)
|
||||
- build(deps): Bump mkdocs from 1.4.2 to 1.4.3 in /docs
|
||||
[#471](https://github.com/strukturag/nextcloud-spreed-signaling/pull/471)
|
||||
- build(deps): Bump sphinx-rtd-theme from 1.2.0 to 1.2.1 in /docs
|
||||
[#479](https://github.com/strukturag/nextcloud-spreed-signaling/pull/479)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.1.0 to 2.1.2
|
||||
[#466](https://github.com/strukturag/nextcloud-spreed-signaling/pull/466)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 3.4.0 to 3.5.0
|
||||
[#481](https://github.com/strukturag/nextcloud-spreed-signaling/pull/481)
|
||||
- Simplify vendoring.
|
||||
[#482](https://github.com/strukturag/nextcloud-spreed-signaling/pull/482)
|
||||
- build(deps): Bump sphinx-rtd-theme from 1.2.1 to 1.2.2 in /docs
|
||||
[#485](https://github.com/strukturag/nextcloud-spreed-signaling/pull/485)
|
||||
- build(deps): Bump coverallsapp/github-action from 2.1.2 to 2.2.0
|
||||
[#484](https://github.com/strukturag/nextcloud-spreed-signaling/pull/484)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.53.0 to 1.55.0
|
||||
[#472](https://github.com/strukturag/nextcloud-spreed-signaling/pull/472)
|
||||
- build(deps): Bump go.etcd.io/etcd/client/v3 from 3.5.7 to 3.5.9
|
||||
[#473](https://github.com/strukturag/nextcloud-spreed-signaling/pull/473)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.24.0 to 1.26.0
|
||||
[#478](https://github.com/strukturag/nextcloud-spreed-signaling/pull/478)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 3.5.0 to 3.6.0
|
||||
[#492](https://github.com/strukturag/nextcloud-spreed-signaling/pull/492)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.15 to 2.9.17
|
||||
[#495](https://github.com/strukturag/nextcloud-spreed-signaling/pull/495)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.17 to 2.9.18
|
||||
[#496](https://github.com/strukturag/nextcloud-spreed-signaling/pull/496)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.1
|
||||
[#493](https://github.com/strukturag/nextcloud-spreed-signaling/pull/493)
|
||||
- docker: Don't build concurrently.
|
||||
[#498](https://github.com/strukturag/nextcloud-spreed-signaling/pull/498)
|
||||
- Use "struct{}" channel if only used as signaling mechanism.
|
||||
[#491](https://github.com/strukturag/nextcloud-spreed-signaling/pull/491)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.55.0 to 1.56.0
|
||||
[#502](https://github.com/strukturag/nextcloud-spreed-signaling/pull/502)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0
|
||||
[#501](https://github.com/strukturag/nextcloud-spreed-signaling/pull/501)
|
||||
- build(deps): Bump github.com/oschwald/maxminddb-golang from 1.10.0 to 1.11.0
|
||||
[#503](https://github.com/strukturag/nextcloud-spreed-signaling/pull/503)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.18 to 2.9.19
|
||||
[#504](https://github.com/strukturag/nextcloud-spreed-signaling/pull/504)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.56.0 to 1.56.1
|
||||
[#505](https://github.com/strukturag/nextcloud-spreed-signaling/pull/505)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.27.0 to 1.27.1
|
||||
[#506](https://github.com/strukturag/nextcloud-spreed-signaling/pull/506)
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.30.0 to 1.31.0
|
||||
[#507](https://github.com/strukturag/nextcloud-spreed-signaling/pull/507)
|
||||
|
||||
### Fixed
|
||||
- CI: Make sure proxy Docker image is never tagged as "latest".
|
||||
[#445](https://github.com/strukturag/nextcloud-spreed-signaling/pull/445)
|
||||
- Write backends comma-separated to config
|
||||
[#487](https://github.com/strukturag/nextcloud-spreed-signaling/pull/487)
|
||||
- Fix duplicate join events
|
||||
[#490](https://github.com/strukturag/nextcloud-spreed-signaling/pull/490)
|
||||
- Add missing lock for "roomSessionId" to avoid potential races.
|
||||
[#497](https://github.com/strukturag/nextcloud-spreed-signaling/pull/497)
|
||||
|
||||
|
||||
## 1.1.2 - 2023-03-13
|
||||
|
||||
### Added
|
||||
- Allow SKIP_VERIFY in docker image.
|
||||
[#430](https://github.com/strukturag/nextcloud-spreed-signaling/pull/430)
|
||||
|
||||
### Changed
|
||||
- Keep Docker images alpine based.
|
||||
[#427](https://github.com/strukturag/nextcloud-spreed-signaling/pull/427)
|
||||
- build(deps): Bump coverallsapp/github-action from 1.1.3 to 1.2.0
|
||||
[#433](https://github.com/strukturag/nextcloud-spreed-signaling/pull/433)
|
||||
- build(deps): Bump coverallsapp/github-action from 1.2.0 to 1.2.2
|
||||
[#435](https://github.com/strukturag/nextcloud-spreed-signaling/pull/435)
|
||||
- build(deps): Bump coverallsapp/github-action from 1.2.2 to 1.2.3
|
||||
[#436](https://github.com/strukturag/nextcloud-spreed-signaling/pull/436)
|
||||
- build(deps): Bump coverallsapp/github-action from 1.2.3 to 1.2.4
|
||||
[#437](https://github.com/strukturag/nextcloud-spreed-signaling/pull/437)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.23.0 to 1.24.0
|
||||
[#434](https://github.com/strukturag/nextcloud-spreed-signaling/pull/434)
|
||||
- Run "go mod tidy -compat=1.18".
|
||||
[#440](https://github.com/strukturag/nextcloud-spreed-signaling/pull/440)
|
||||
- CI: Run golangci-lint with Go 1.20
|
||||
- Update protoc-gen-go-grpc to v1.3.0
|
||||
[#442](https://github.com/strukturag/nextcloud-spreed-signaling/pull/442)
|
||||
- CI: Stop using deprecated "set-output".
|
||||
[#441](https://github.com/strukturag/nextcloud-spreed-signaling/pull/441)
|
||||
- docker: Don't rely on default values when updating TURN settings.
|
||||
[#439](https://github.com/strukturag/nextcloud-spreed-signaling/pull/439)
|
||||
- build(deps): Bump google.golang.org/protobuf from 1.28.1 to 1.29.0
|
||||
[#443](https://github.com/strukturag/nextcloud-spreed-signaling/pull/443)
|
||||
|
||||
### Fixed
|
||||
- Fix example in docker README.
|
||||
[#429](https://github.com/strukturag/nextcloud-spreed-signaling/pull/429)
|
||||
- TURN_API_KEY and TURN_SECRET fix.
|
||||
[#428](https://github.com/strukturag/nextcloud-spreed-signaling/pull/428)
|
||||
|
||||
|
||||
## 1.1.1 - 2023-02-22
|
||||
|
||||
### Fixed
|
||||
- Fix Docker images.
|
||||
[#425](https://github.com/strukturag/nextcloud-spreed-signaling/pull/425)
|
||||
|
||||
|
||||
## 1.1.0 - 2023-02-22
|
||||
|
||||
### Added
|
||||
- Official docker images.
|
||||
[#314](https://github.com/strukturag/nextcloud-spreed-signaling/pull/314)
|
||||
- Use proxy from environment for backend client requests.
|
||||
[#326](https://github.com/strukturag/nextcloud-spreed-signaling/pull/326)
|
||||
- Add aarch64/arm64 docker build
|
||||
[#384](https://github.com/strukturag/nextcloud-spreed-signaling/pull/384)
|
||||
- CI: Setup permissions for workflows.
|
||||
[#393](https://github.com/strukturag/nextcloud-spreed-signaling/pull/393)
|
||||
- Implement "switchto" support
|
||||
[#409](https://github.com/strukturag/nextcloud-spreed-signaling/pull/409)
|
||||
- Allow internal clients to set / change the "inCall" flags.
|
||||
[#421](https://github.com/strukturag/nextcloud-spreed-signaling/pull/421)
|
||||
- Add support for Golang 1.20
|
||||
[#413](https://github.com/strukturag/nextcloud-spreed-signaling/pull/413)
|
||||
|
||||
### Changed
|
||||
- Switch to apt-get on CLI.
|
||||
[#312](https://github.com/strukturag/nextcloud-spreed-signaling/pull/312)
|
||||
- vendor: Automatically vendor protobuf modules.
|
||||
[#313](https://github.com/strukturag/nextcloud-spreed-signaling/pull/313)
|
||||
- Bump github.com/prometheus/client_golang from 1.12.2 to 1.13.0
|
||||
[#316](https://github.com/strukturag/nextcloud-spreed-signaling/pull/316)
|
||||
- Bump github.com/oschwald/maxminddb-golang from 1.9.0 to 1.10.0
|
||||
[#317](https://github.com/strukturag/nextcloud-spreed-signaling/pull/317)
|
||||
- Bump github.com/pion/sdp/v3 from 3.0.5 to 3.0.6
|
||||
[#320](https://github.com/strukturag/nextcloud-spreed-signaling/pull/320)
|
||||
- Bump google.golang.org/grpc from 1.48.0 to 1.49.0
|
||||
[#324](https://github.com/strukturag/nextcloud-spreed-signaling/pull/324)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.8.4 to 2.9.0
|
||||
[#330](https://github.com/strukturag/nextcloud-spreed-signaling/pull/330)
|
||||
- Bump sphinx from 5.1.1 to 5.2.2 in /docs
|
||||
[#339](https://github.com/strukturag/nextcloud-spreed-signaling/pull/339)
|
||||
- Bump mkdocs from 1.3.1 to 1.4.0 in /docs
|
||||
[#340](https://github.com/strukturag/nextcloud-spreed-signaling/pull/340)
|
||||
- Bump sphinx from 5.2.2 to 5.2.3 in /docs
|
||||
[#345](https://github.com/strukturag/nextcloud-spreed-signaling/pull/345)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.9.0 to 2.9.2
|
||||
[#344](https://github.com/strukturag/nextcloud-spreed-signaling/pull/344)
|
||||
- Bump go.etcd.io/etcd/api/v3 from 3.5.4 to 3.5.5
|
||||
[#333](https://github.com/strukturag/nextcloud-spreed-signaling/pull/333)
|
||||
- Bump go.etcd.io/etcd/server/v3 from 3.5.4 to 3.5.5
|
||||
[#334](https://github.com/strukturag/nextcloud-spreed-signaling/pull/334)
|
||||
- Bump google.golang.org/grpc from 1.49.0 to 1.50.0
|
||||
[#346](https://github.com/strukturag/nextcloud-spreed-signaling/pull/346)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.9.2 to 2.9.3
|
||||
[#348](https://github.com/strukturag/nextcloud-spreed-signaling/pull/348)
|
||||
- Bump github.com/nats-io/nats.go from 1.17.0 to 1.18.0
|
||||
[#349](https://github.com/strukturag/nextcloud-spreed-signaling/pull/349)
|
||||
- Bump sphinx from 5.2.3 to 5.3.0 in /docs
|
||||
[#351](https://github.com/strukturag/nextcloud-spreed-signaling/pull/351)
|
||||
- Bump mkdocs from 1.4.0 to 1.4.1 in /docs
|
||||
[#352](https://github.com/strukturag/nextcloud-spreed-signaling/pull/352)
|
||||
- Bump google.golang.org/grpc from 1.50.0 to 1.50.1
|
||||
[#350](https://github.com/strukturag/nextcloud-spreed-signaling/pull/350)
|
||||
- Bump golangci/golangci-lint-action from 3.2.0 to 3.3.0
|
||||
[#353](https://github.com/strukturag/nextcloud-spreed-signaling/pull/353)
|
||||
- Bump mkdocs from 1.4.1 to 1.4.2 in /docs
|
||||
[#358](https://github.com/strukturag/nextcloud-spreed-signaling/pull/358)
|
||||
- Bump sphinx-rtd-theme from 1.0.0 to 1.1.0 in /docs
|
||||
[#357](https://github.com/strukturag/nextcloud-spreed-signaling/pull/357)
|
||||
- Bump github.com/nats-io/nats.go from 1.18.0 to 1.19.0
|
||||
[#354](https://github.com/strukturag/nextcloud-spreed-signaling/pull/354)
|
||||
- Bump github.com/prometheus/client_golang from 1.13.0 to 1.13.1
|
||||
[#360](https://github.com/strukturag/nextcloud-spreed-signaling/pull/360)
|
||||
- Bump github.com/nats-io/nats-server/v2 from 2.9.3 to 2.9.5
|
||||
[#359](https://github.com/strukturag/nextcloud-spreed-signaling/pull/359)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 3.3.0 to 3.3.1
|
||||
[#365](https://github.com/strukturag/nextcloud-spreed-signaling/pull/365)
|
||||
- build(deps): Bump sphinx-rtd-theme from 1.1.0 to 1.1.1 in /docs
|
||||
[#363](https://github.com/strukturag/nextcloud-spreed-signaling/pull/363)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.5 to 2.9.6
|
||||
[#361](https://github.com/strukturag/nextcloud-spreed-signaling/pull/361)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.19.0 to 1.20.0
|
||||
[#366](https://github.com/strukturag/nextcloud-spreed-signaling/pull/366)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.50.1 to 1.51.0
|
||||
[#368](https://github.com/strukturag/nextcloud-spreed-signaling/pull/368)
|
||||
- build(deps): Bump github.com/prometheus/client_golang from 1.13.1 to 1.14.0
|
||||
[#364](https://github.com/strukturag/nextcloud-spreed-signaling/pull/364)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.6 to 2.9.7
|
||||
[#367](https://github.com/strukturag/nextcloud-spreed-signaling/pull/367)
|
||||
- build(deps): Bump go.etcd.io/etcd/server/v3 from 3.5.5 to 3.5.6
|
||||
[#372](https://github.com/strukturag/nextcloud-spreed-signaling/pull/372)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.7 to 2.9.8
|
||||
[#371](https://github.com/strukturag/nextcloud-spreed-signaling/pull/371)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.20.0 to 1.21.0
|
||||
[#375](https://github.com/strukturag/nextcloud-spreed-signaling/pull/375)
|
||||
- build(deps): Bump github.com/golang-jwt/jwt/v4 from 4.4.2 to 4.4.3
|
||||
[#374](https://github.com/strukturag/nextcloud-spreed-signaling/pull/374)
|
||||
- build(deps): Bump cirrus-actions/rebase from 1.7 to 1.8
|
||||
[#379](https://github.com/strukturag/nextcloud-spreed-signaling/pull/379)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.8 to 2.9.9
|
||||
[#377](https://github.com/strukturag/nextcloud-spreed-signaling/pull/377)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.9 to 2.9.10
|
||||
[#382](https://github.com/strukturag/nextcloud-spreed-signaling/pull/382)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.21.0 to 1.22.1
|
||||
[#383](https://github.com/strukturag/nextcloud-spreed-signaling/pull/383)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.51.0 to 1.52.0
|
||||
[#391](https://github.com/strukturag/nextcloud-spreed-signaling/pull/391)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.10 to 2.9.11
|
||||
[#387](https://github.com/strukturag/nextcloud-spreed-signaling/pull/387)
|
||||
- Stop using WaitGroup to detect finished message processing.
|
||||
[#394](https://github.com/strukturag/nextcloud-spreed-signaling/pull/394)
|
||||
- Improve handling of throttled responses from Nextcloud.
|
||||
[#395](https://github.com/strukturag/nextcloud-spreed-signaling/pull/395)
|
||||
- Test: add timeout while waiting for etcd event.
|
||||
[#397](https://github.com/strukturag/nextcloud-spreed-signaling/pull/397)
|
||||
- build(deps): Bump github.com/nats-io/nats.go from 1.22.1 to 1.23.0
|
||||
[#399](https://github.com/strukturag/nextcloud-spreed-signaling/pull/399)
|
||||
- build(deps): Bump go.etcd.io/etcd/api/v3 from 3.5.6 to 3.5.7
|
||||
[#402](https://github.com/strukturag/nextcloud-spreed-signaling/pull/402)
|
||||
- build(deps): Bump go.etcd.io/etcd/client/v3 from 3.5.6 to 3.5.7
|
||||
[#403](https://github.com/strukturag/nextcloud-spreed-signaling/pull/403)
|
||||
- build(deps): Bump go.etcd.io/etcd/server/v3 from 3.5.6 to 3.5.7
|
||||
[#404](https://github.com/strukturag/nextcloud-spreed-signaling/pull/404)
|
||||
- build(deps): Bump golangci/golangci-lint-action from 3.3.1 to 3.4.0
|
||||
[#405](https://github.com/strukturag/nextcloud-spreed-signaling/pull/405)
|
||||
- build(deps): Bump readthedocs-sphinx-search from 0.1.2 to 0.2.0 in /docs
|
||||
[#407](https://github.com/strukturag/nextcloud-spreed-signaling/pull/407)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.52.0 to 1.52.1
|
||||
[#406](https://github.com/strukturag/nextcloud-spreed-signaling/pull/406)
|
||||
- build(deps): Bump docker/build-push-action from 3 to 4
|
||||
[#412](https://github.com/strukturag/nextcloud-spreed-signaling/pull/412)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.52.1 to 1.52.3
|
||||
[#410](https://github.com/strukturag/nextcloud-spreed-signaling/pull/410)
|
||||
- Explicitly use type "sysConn".
|
||||
[#416](https://github.com/strukturag/nextcloud-spreed-signaling/pull/416)
|
||||
- build(deps): Bump github.com/nats-io/nats-server/v2 from 2.9.11 to 2.9.14
|
||||
[#415](https://github.com/strukturag/nextcloud-spreed-signaling/pull/415)
|
||||
- build(deps): Bump sphinx-rtd-theme from 1.1.1 to 1.2.0 in /docs
|
||||
[#418](https://github.com/strukturag/nextcloud-spreed-signaling/pull/418)
|
||||
- build(deps): Bump google.golang.org/grpc from 1.52.3 to 1.53.0
|
||||
[#417](https://github.com/strukturag/nextcloud-spreed-signaling/pull/417)
|
||||
- build(deps): Bump golang.org/x/net from 0.5.0 to 0.7.0
|
||||
[#422](https://github.com/strukturag/nextcloud-spreed-signaling/pull/422)
|
||||
- build(deps): Bump github.com/golang-jwt/jwt/v4 from 4.4.3 to 4.5.0
|
||||
[#423](https://github.com/strukturag/nextcloud-spreed-signaling/pull/423)
|
||||
- build(deps): Bump sphinx from 5.3.0 to 6.1.3 in /docs
|
||||
[#390](https://github.com/strukturag/nextcloud-spreed-signaling/pull/390)
|
||||
- Various refactorings to simplify code
|
||||
[#400](https://github.com/strukturag/nextcloud-spreed-signaling/pull/400)
|
||||
|
||||
### Fixed
|
||||
- Remove @resources from SystemCallFilter
|
||||
[#322](https://github.com/strukturag/nextcloud-spreed-signaling/pull/322)
|
||||
- Fix deadlock for proxy connection issues
|
||||
[#327](https://github.com/strukturag/nextcloud-spreed-signaling/pull/327)
|
||||
- Fix goroutines leak check.
|
||||
[#396](https://github.com/strukturag/nextcloud-spreed-signaling/pull/396)
|
||||
|
||||
|
||||
## 1.0.0 - 2022-08-04
|
||||
|
||||
### Added
|
||||
- Clustering support.
|
||||
[#281](https://github.com/strukturag/nextcloud-spreed-signaling/pull/281)
|
||||
- Send initial "welcome" message when clients connect.
|
||||
[#288](https://github.com/strukturag/nextcloud-spreed-signaling/pull/288)
|
||||
- Support hello auth version "2.0" with JWT.
|
||||
[#251](https://github.com/strukturag/nextcloud-spreed-signaling/pull/251)
|
||||
- dist: add systemd sysusers file.
|
||||
[#275](https://github.com/strukturag/nextcloud-spreed-signaling/pull/275)
|
||||
- Add more tests.
|
||||
[#292](https://github.com/strukturag/nextcloud-spreed-signaling/pull/292)
|
||||
- Add tests for virtual sessions.
|
||||
[#295](https://github.com/strukturag/nextcloud-spreed-signaling/pull/295)
|
||||
- Implement per-backend session limit for clusters.
|
||||
[#296](https://github.com/strukturag/nextcloud-spreed-signaling/pull/296)
|
||||
|
||||
### Changed
|
||||
- Don't run "go mod tidy" when building.
|
||||
[#269](https://github.com/strukturag/nextcloud-spreed-signaling/pull/269)
|
||||
- Bump sphinx from 5.0.0 to 5.0.1 in /docs
|
||||
[#270](https://github.com/strukturag/nextcloud-spreed-signaling/pull/270)
|
||||
- Bump sphinx from 5.0.1 to 5.0.2 in /docs
|
||||
[#277](https://github.com/strukturag/nextcloud-spreed-signaling/pull/277)
|
||||
- Move common etcd code to own class.
|
||||
[#282](https://github.com/strukturag/nextcloud-spreed-signaling/pull/282)
|
||||
- Support arbitrary capabilities values.
|
||||
[#287](https://github.com/strukturag/nextcloud-spreed-signaling/pull/287)
|
||||
- dist: harden systemd service unit.
|
||||
[#276](https://github.com/strukturag/nextcloud-spreed-signaling/pull/276)
|
||||
- Update to Go module version of github.com/golang-jwt/jwt
|
||||
[#289](https://github.com/strukturag/nextcloud-spreed-signaling/pull/289)
|
||||
- Disconnect sessions with the same room session id synchronously.
|
||||
[#294](https://github.com/strukturag/nextcloud-spreed-signaling/pull/294)
|
||||
- Bump google.golang.org/grpc from 1.47.0 to 1.48.0
|
||||
[#297](https://github.com/strukturag/nextcloud-spreed-signaling/pull/297)
|
||||
- Update to github.com/pion/sdp v3.0.5
|
||||
[#301](https://github.com/strukturag/nextcloud-spreed-signaling/pull/301)
|
||||
- Bump sphinx from 5.0.2 to 5.1.1 in /docs
|
||||
[#303](https://github.com/strukturag/nextcloud-spreed-signaling/pull/303)
|
||||
- make: Include vendored dependencies in tarball.
|
||||
[#300](https://github.com/strukturag/nextcloud-spreed-signaling/pull/300)
|
||||
- docs: update and pin dependencies.
|
||||
[#305](https://github.com/strukturag/nextcloud-spreed-signaling/pull/305)
|
||||
- Bump actions/upload-artifact from 2 to 3
|
||||
[#307](https://github.com/strukturag/nextcloud-spreed-signaling/pull/307)
|
||||
- Bump actions/download-artifact from 2 to 3
|
||||
[#308](https://github.com/strukturag/nextcloud-spreed-signaling/pull/308)
|
||||
- Bump google.golang.org/protobuf from 1.28.0 to 1.28.1
|
||||
[#306](https://github.com/strukturag/nextcloud-spreed-signaling/pull/306)
|
||||
- CI: Also test with Golang 1.19
|
||||
[#310](https://github.com/strukturag/nextcloud-spreed-signaling/pull/310)
|
||||
|
||||
### Fixed
|
||||
- Fix check for async room messages received while not joined to a room.
|
||||
[#274](https://github.com/strukturag/nextcloud-spreed-signaling/pull/274)
|
||||
- Fix testing etcd server not starting up if etcd is running on host.
|
||||
[#283](https://github.com/strukturag/nextcloud-spreed-signaling/pull/283)
|
||||
- Fix CI issues on slow CPUs.
|
||||
[#290](https://github.com/strukturag/nextcloud-spreed-signaling/pull/290)
|
||||
- Fix handling of "unshareScreen" messages and add test.
|
||||
[#293](https://github.com/strukturag/nextcloud-spreed-signaling/pull/293)
|
||||
- Fix Read The Ddocs builds.
|
||||
[#302](https://github.com/strukturag/nextcloud-spreed-signaling/pull/302)
|
||||
|
||||
|
||||
## 0.5.0 - 2022-06-02
|
||||
|
||||
### Added
|
||||
|
|
17
Dockerfile
17
Dockerfile
|
@ -1,17 +0,0 @@
|
|||
FROM golang:1.18 AS builder
|
||||
|
||||
WORKDIR /workdir
|
||||
|
||||
COPY . .
|
||||
RUN make build
|
||||
|
||||
FROM alpine:3.15
|
||||
|
||||
ENV CONFIG=/config/server.conf
|
||||
RUN adduser -D spreedbackend && \
|
||||
apk add --no-cache ca-certificates libc6-compat libstdc++
|
||||
USER spreedbackend
|
||||
COPY --from=builder /workdir/bin/signaling /usr/local/signaling
|
||||
COPY ./server.conf.in /config/server.conf
|
||||
|
||||
CMD ["/bin/sh", "-c", "/usr/local/signaling --config=$CONFIG"]
|
97
Makefile
97
Makefile
|
@ -7,11 +7,20 @@ GOFMT := "$(GODIR)/gofmt"
|
|||
GOOS ?= linux
|
||||
GOARCH ?= amd64
|
||||
GOVERSION := $(shell "$(GO)" env GOVERSION | sed "s|go||" )
|
||||
BINDIR := "$(CURDIR)/bin"
|
||||
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
|
||||
PROTO_FILES := $(basename $(wildcard *.proto))
|
||||
PROTO_GO_FILES := $(addsuffix .pb.go,$(PROTO_FILES)) $(addsuffix _grpc.pb.go,$(PROTO_FILES))
|
||||
EASYJSON_GO_FILES := \
|
||||
api_async_easyjson.go \
|
||||
api_backend_easyjson.go \
|
||||
api_grpc_easyjson.go \
|
||||
api_proxy_easyjson.go \
|
||||
api_signaling_easyjson.go
|
||||
|
||||
ifneq ($(VERSION),)
|
||||
INTERNALLDFLAGS := -X main.version=$(VERSION)
|
||||
|
@ -36,13 +45,21 @@ TIMEOUT := 60s
|
|||
endif
|
||||
|
||||
ifneq ($(TEST),)
|
||||
TESTARGS := $(TESTARGS) -run $(TEST)
|
||||
TESTARGS := $(TESTARGS) -run "$(TEST)"
|
||||
endif
|
||||
|
||||
ifneq ($(COUNT),)
|
||||
TESTARGS := $(TESTARGS) -count $(COUNT)
|
||||
endif
|
||||
|
||||
ifneq ($(PARALLEL),)
|
||||
TESTARGS := $(TESTARGS) -parallel $(PARALLEL)
|
||||
endif
|
||||
|
||||
ifneq ($(VERBOSE),)
|
||||
TESTARGS := $(TESTARGS) -v
|
||||
endif
|
||||
|
||||
ifeq ($(GOARCH), amd64)
|
||||
GOPATHBIN := $(GOPATH)/bin
|
||||
else
|
||||
|
@ -52,10 +69,17 @@ endif
|
|||
hook:
|
||||
[ ! -d "$(CURDIR)/.git/hooks" ] || ln -sf "$(CURDIR)/scripts/pre-commit.hook" "$(CURDIR)/.git/hooks/pre-commit"
|
||||
|
||||
$(GOPATHBIN)/easyjson:
|
||||
$(GO) get -u -d github.com/mailru/easyjson/...
|
||||
$(GOPATHBIN)/easyjson: go.mod go.sum
|
||||
[ "$(GOPROXY)" = "off" ] || $(GO) get -d github.com/mailru/easyjson/...
|
||||
$(GO) install github.com/mailru/easyjson/...
|
||||
|
||||
$(GOPATHBIN)/protoc-gen-go: go.mod go.sum
|
||||
$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go
|
||||
|
||||
$(GOPATHBIN)/protoc-gen-go-grpc: go.mod go.sum
|
||||
[ "$(GOPROXY)" = "off" ] || $(GO) get -d google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
||||
$(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
||||
|
||||
continentmap.go:
|
||||
$(CURDIR)/scripts/get_continent_map.py $@
|
||||
|
||||
|
@ -70,62 +94,85 @@ check-continentmap:
|
|||
get:
|
||||
$(GO) get $(PACKAGE)
|
||||
|
||||
fmt: hook
|
||||
fmt: hook | $(PROTO_GO_FILES)
|
||||
$(GOFMT) -s -w *.go client proxy server
|
||||
|
||||
vet: common
|
||||
$(GO) vet $(ALL_PACKAGES)
|
||||
|
||||
test: vet common
|
||||
$(GO) test -v -timeout $(TIMEOUT) $(TESTARGS) $(ALL_PACKAGES)
|
||||
$(GO) test -timeout $(TIMEOUT) $(TESTARGS) $(ALL_PACKAGES)
|
||||
|
||||
cover: vet common
|
||||
rm -f cover.out && \
|
||||
$(GO) test -v -timeout $(TIMEOUT) -coverprofile cover.out $(ALL_PACKAGES) && \
|
||||
$(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
|
||||
|
||||
coverhtml: vet common
|
||||
rm -f cover.out && \
|
||||
$(GO) test -v -timeout $(TIMEOUT) -coverprofile cover.out $(ALL_PACKAGES) && \
|
||||
$(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 -html=cover.out -o coverage.html
|
||||
|
||||
%_easyjson.go: %.go $(GOPATHBIN)/easyjson
|
||||
%_easyjson.go: %.go $(GOPATHBIN)/easyjson | $(PROTO_GO_FILES)
|
||||
rm -f easyjson-bootstrap*.go
|
||||
PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $*.go
|
||||
|
||||
common: \
|
||||
api_signaling_easyjson.go \
|
||||
api_backend_easyjson.go \
|
||||
api_proxy_easyjson.go \
|
||||
natsclient_easyjson.go \
|
||||
room_easyjson.go
|
||||
$(GO) mod tidy
|
||||
%.pb.go: %.proto $(GOPATHBIN)/protoc-gen-go $(GOPATHBIN)/protoc-gen-go-grpc
|
||||
PATH="$(GODIR)":"$(GOPATHBIN)":$(PATH) protoc \
|
||||
--go_out=. --go_opt=paths=source_relative \
|
||||
$*.proto
|
||||
|
||||
%_grpc.pb.go: %.proto $(GOPATHBIN)/protoc-gen-go $(GOPATHBIN)/protoc-gen-go-grpc
|
||||
PATH="$(GODIR)":"$(GOPATHBIN)":$(PATH) protoc \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
$*.proto
|
||||
|
||||
common: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
mkdir -p "$(BINDIR)"
|
||||
|
||||
client: common $(BINDIR)
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $(BINDIR)/client ./client/...
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o "$(BINDIR)/client" ./client/...
|
||||
|
||||
server: common $(BINDIR)
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $(BINDIR)/signaling ./server/...
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o "$(BINDIR)/signaling" ./server/...
|
||||
|
||||
proxy: common $(BINDIR)
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $(BINDIR)/proxy ./proxy/...
|
||||
$(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o "$(BINDIR)/proxy" ./proxy/...
|
||||
|
||||
clean:
|
||||
rm -f *_easyjson.go
|
||||
rm -f $(EASYJSON_GO_FILES)
|
||||
rm -f easyjson-bootstrap*.go
|
||||
rm -f $(PROTO_GO_FILES)
|
||||
|
||||
build: server proxy
|
||||
|
||||
tarball:
|
||||
vendor: go.mod go.sum common
|
||||
set -e ;\
|
||||
rm -rf $(VENDORDIR)
|
||||
$(GO) mod tidy; \
|
||||
$(GO) mod vendor
|
||||
|
||||
tarball: vendor
|
||||
git archive \
|
||||
--prefix=nextcloud-spreed-signaling-$(TARVERSION)/ \
|
||||
-o nextcloud-spreed-signaling-$(TARVERSION).tar.gz \
|
||||
-o nextcloud-spreed-signaling-$(TARVERSION).tar \
|
||||
HEAD
|
||||
tar rf nextcloud-spreed-signaling-$(TARVERSION).tar \
|
||||
-C $(CURDIR) \
|
||||
--mtime="$(shell git log -1 --date=iso8601-strict --format=%cd HEAD)" \
|
||||
--transform "s//nextcloud-spreed-signaling-$(TARVERSION)\//" \
|
||||
vendor
|
||||
gzip --force nextcloud-spreed-signaling-$(TARVERSION).tar
|
||||
|
||||
dist: tarball
|
||||
|
||||
.NOTPARALLEL: %_easyjson.go
|
||||
.PHONY: continentmap.go
|
||||
.NOTPARALLEL: $(EASYJSON_GO_FILES)
|
||||
.PHONY: continentmap.go common vendor
|
||||
.SECONDARY: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES)
|
||||
.DELETE_ON_ERROR:
|
||||
|
|
70
README.md
70
README.md
|
@ -8,7 +8,7 @@
|
|||
This repository contains the standalone signaling server which can be used for
|
||||
Nextcloud Talk (https://apps.nextcloud.com/apps/spreed).
|
||||
|
||||
See https://nextcloud-talk.readthedocs.io/en/latest/standalone-signaling-api-v1/ for further
|
||||
See https://nextcloud-spreed-signaling.readthedocs.io/en/latest/ for further
|
||||
information on the API of the signaling server.
|
||||
|
||||
|
||||
|
@ -17,8 +17,12 @@ information on the API of the signaling server.
|
|||
The following tools are required for building the signaling server.
|
||||
|
||||
- git
|
||||
- go >= 1.17
|
||||
- go >= 1.21
|
||||
- make
|
||||
- protobuf-compiler >= 3
|
||||
|
||||
Usually the last two versions of Go are supported. This follows the release
|
||||
policy of Go: https://go.dev/doc/devel/release#policy
|
||||
|
||||
All other dependencies are fetched automatically while building.
|
||||
|
||||
|
@ -87,23 +91,34 @@ systemctl start signaling.service
|
|||
|
||||
### Running with Docker
|
||||
|
||||
Official docker containers 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.
|
||||
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
You will likely have to adjust the Janus command line options depending on the exact network configuration on your server. Refer to [Setup of Janus](#setup-of-janus) and the Janus documentation for how to configure your Janus server.
|
||||
|
||||
Copy `server.conf.in` to `server.conf` and adjust it to your liking.
|
||||
|
||||
If you're using the [docker-compose.yml](docker-compose.yml) configuration as is, the MCU Url must be set to `ws://localhost:8188`, the NATS Url must be set to `nats://localhost:4222`, and TURN Servers must be set to `turn:localhost:3478?transport=udp,turn:localhost:3478?transport=tcp`.
|
||||
If you're using the [docker-compose.yml](docker/docker-compose.yml) configuration as is, the MCU Url must be set to `ws://localhost:8188`, the NATS Url must be set to `nats://localhost:4222`, and TURN Servers must be set to `turn:localhost:3478?transport=udp,turn:localhost:3478?transport=tcp`.
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Please note that docker-compose v2 is required for building while most
|
||||
distributions will ship older versions. You can download a recent version from
|
||||
https://docs.docker.com/compose/install/
|
||||
|
||||
|
||||
## Setup of NATS server
|
||||
|
||||
There is a detailed description on how to install and run the NATS server
|
||||
available at http://nats.io/documentation/tutorials/gnatsd-install/
|
||||
available at https://docs.nats.io/running-a-nats-service/introduction
|
||||
|
||||
You can use the `gnatsd.conf` file as base for the configuration of the NATS
|
||||
server.
|
||||
|
@ -156,6 +171,30 @@ proxy process gracefully after all clients have been disconnected. No new
|
|||
publishers will be accepted in this case.
|
||||
|
||||
|
||||
### Remote streams (preview)
|
||||
|
||||
With Janus 1.1.0 or newer, remote streams are supported, i.e. a subscriber can
|
||||
receive a published stream from any server. For this, you need to configure
|
||||
`hostname`, `token_id` and `token_key` in the proxy configuration. Each proxy
|
||||
server also supports configuring maximum `incoming` and `outgoing` bandwidth
|
||||
settings, which will also be used to select remote streams.
|
||||
See `proxy.conf.in` in section `app` for details.
|
||||
|
||||
|
||||
## Clustering
|
||||
|
||||
The signaling server supports a clustering mode where multiple running servers
|
||||
can be interconnected to form a single "virtual" server. This can be used to
|
||||
increase the capacity of the signaling server or provide a failover setup.
|
||||
|
||||
For that a central NATS server / cluster must be used by all instances. Each
|
||||
instance must run a GRPC server (enable `listening` in section `grpc` and
|
||||
optionally setup certificate, private key and CA). The list of other GRPC
|
||||
targets must be configured as `targets` in section `grpc` or can be retrieved
|
||||
from an etcd cluster. See `server.conf.in` in section `grpc` for configuration
|
||||
details.
|
||||
|
||||
|
||||
## Setup of frontend webserver
|
||||
|
||||
Usually the standalone signaling server is running behind a webserver that does
|
||||
|
@ -270,6 +309,8 @@ interface on port `8080` below):
|
|||
# Enable proxying Websocket requests to the standalone signaling server.
|
||||
ProxyPass "/standalone-signaling/" "ws://127.0.0.1:8080/"
|
||||
|
||||
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
|
||||
|
||||
RewriteEngine On
|
||||
# Websocket connections from the clients.
|
||||
RewriteRule ^/standalone-signaling/spreed/$ - [L]
|
||||
|
@ -305,6 +346,7 @@ myserver.domain.invalid {
|
|||
route /standalone-signaling/* {
|
||||
uri strip_prefix /standalone-signaling
|
||||
reverse_proxy http://127.0.0.1:8080
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -351,23 +393,3 @@ Usage:
|
|||
config file to use (default "server.conf")
|
||||
-maxClients int
|
||||
number of client connections (default 100)
|
||||
|
||||
|
||||
## Running multiple signaling servers
|
||||
|
||||
IMPORTANT: This is considered experimental and might not work with all
|
||||
functionality of the signaling server, especially when using the Janus
|
||||
integration.
|
||||
|
||||
The signaling server uses the NATS server to send messages to peers that are
|
||||
not connected locally. Therefore multiple signaling servers running on different
|
||||
hosts can use the same NATS server to build a simple cluster, allowing more
|
||||
simultaneous connections and distribute the load.
|
||||
|
||||
To set this up, make sure all signaling servers are using the same settings for
|
||||
their `session` keys and the `secret` in the `backend` section. Also the URL to
|
||||
the NATS server (option `url` in section `nats`) must point to the same NATS
|
||||
server.
|
||||
|
||||
If all this is setup correctly, clients can connect to either of the signaling
|
||||
servers and exchange messages between them.
|
||||
|
|
134
allowed_ips.go
Normal file
134
allowed_ips.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AllowedIps struct {
|
||||
allowed []*net.IPNet
|
||||
}
|
||||
|
||||
func (a *AllowedIps) String() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("[")
|
||||
for idx, n := range a.allowed {
|
||||
if idx > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(n.String())
|
||||
}
|
||||
b.WriteString("]")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (a *AllowedIps) Empty() bool {
|
||||
return len(a.allowed) == 0
|
||||
}
|
||||
|
||||
func (a *AllowedIps) Allowed(ip net.IP) bool {
|
||||
for _, i := range a.allowed {
|
||||
if i.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func parseIPNet(s string) (*net.IPNet, error) {
|
||||
var ipnet *net.IPNet
|
||||
if strings.ContainsRune(s, '/') {
|
||||
var err error
|
||||
if _, ipnet, err = net.ParseCIDR(s); err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address/subnet %s: %w", s, err)
|
||||
}
|
||||
} else {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address %s", s)
|
||||
}
|
||||
|
||||
ipnet = &net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(len(ip)*8, len(ip)*8),
|
||||
}
|
||||
}
|
||||
|
||||
return ipnet, nil
|
||||
}
|
||||
|
||||
func ParseAllowedIps(allowed string) (*AllowedIps, 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)
|
||||
}
|
||||
}
|
||||
|
||||
result := &AllowedIps{
|
||||
allowed: allowedIps,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func DefaultAllowedIps() *AllowedIps {
|
||||
allowedIps := []*net.IPNet{
|
||||
{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Mask: net.CIDRMask(32, 32),
|
||||
},
|
||||
}
|
||||
|
||||
result := &AllowedIps{
|
||||
allowed: allowedIps,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var (
|
||||
privateIpNets = []string{
|
||||
// Loopback addresses.
|
||||
"127.0.0.0/8",
|
||||
// Private addresses.
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
}
|
||||
)
|
||||
|
||||
func DefaultPrivateIps() *AllowedIps {
|
||||
allowed, err := ParseAllowedIps(strings.Join(privateIpNets, ","))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not parse private ips %+v: %w", privateIpNets, err))
|
||||
}
|
||||
return allowed
|
||||
}
|
73
allowed_ips_test.go
Normal file
73
allowed_ips_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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 (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllowedIps(t *testing.T) {
|
||||
a, err := ParseAllowedIps("127.0.0.1, 192.168.0.1, 192.168.1.1/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.Empty() {
|
||||
t.Fatal("should not be empty")
|
||||
}
|
||||
if expected := `[127.0.0.1/32, 192.168.0.1/32, 192.168.1.0/24]`; a.String() != expected {
|
||||
t.Errorf("expected %s, got %s", expected, a.String())
|
||||
}
|
||||
|
||||
allowed := []string{
|
||||
"127.0.0.1",
|
||||
"192.168.0.1",
|
||||
"192.168.1.1",
|
||||
"192.168.1.100",
|
||||
}
|
||||
notAllowed := []string{
|
||||
"192.168.0.2",
|
||||
"10.1.2.3",
|
||||
}
|
||||
|
||||
for _, addr := range allowed {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
t.Errorf("error parsing %s", addr)
|
||||
} else if !a.Allowed(ip) {
|
||||
t.Errorf("should allow %s", addr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, addr := range notAllowed {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
t.Errorf("error parsing %s", addr)
|
||||
} else if a.Allowed(ip) {
|
||||
t.Errorf("should not allow %s", addr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
67
api_async.go
Normal file
67
api_async.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AsyncMessage struct {
|
||||
SendTime time.Time `json:"sendtime"`
|
||||
|
||||
Type string `json:"type"`
|
||||
|
||||
Message *ServerMessage `json:"message,omitempty"`
|
||||
|
||||
Room *BackendServerRoomRequest `json:"room,omitempty"`
|
||||
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
|
||||
AsyncRoom *AsyncRoomMessage `json:"asyncroom,omitempty"`
|
||||
|
||||
SendOffer *SendOfferMessage `json:"sendoffer,omitempty"`
|
||||
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
func (m *AsyncMessage) String() string {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Could not serialize %#v: %s", m, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type AsyncRoomMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
SessionId string `json:"sessionid,omitempty"`
|
||||
ClientType string `json:"clienttype,omitempty"`
|
||||
}
|
||||
|
||||
type SendOfferMessage struct {
|
||||
MessageId string `json:"messageid,omitempty"`
|
||||
SessionId string `json:"sessionid"`
|
||||
Data *MessageClientMessageData `json:"data"`
|
||||
}
|
179
api_backend.go
179
api_backend.go
|
@ -28,7 +28,12 @@ import (
|
|||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -37,6 +42,11 @@ const (
|
|||
HeaderBackendSignalingRandom = "Spreed-Signaling-Random"
|
||||
HeaderBackendSignalingChecksum = "Spreed-Signaling-Checksum"
|
||||
HeaderBackendServer = "Spreed-Signaling-Backend"
|
||||
|
||||
ConfigGroupSignaling = "signaling"
|
||||
|
||||
ConfigKeyHelloV2TokenKey = "hello-v2-token-key"
|
||||
ConfigKeySessionPingLimit = "session-ping-limit"
|
||||
)
|
||||
|
||||
func newRandomString(length int) string {
|
||||
|
@ -94,6 +104,12 @@ type BackendServerRoomRequest struct {
|
|||
|
||||
Message *BackendRoomMessageRequest `json:"message,omitempty"`
|
||||
|
||||
SwitchTo *BackendRoomSwitchToMessageRequest `json:"switchto,omitempty"`
|
||||
|
||||
Dialout *BackendRoomDialoutRequest `json:"dialout,omitempty"`
|
||||
|
||||
Transient *BackendRoomTransientRequest `json:"transient,omitempty"`
|
||||
|
||||
// Internal properties
|
||||
ReceivedTime int64 `json:"received,omitempty"`
|
||||
}
|
||||
|
@ -102,8 +118,8 @@ type BackendRoomInviteRequest struct {
|
|||
UserIds []string `json:"userids,omitempty"`
|
||||
// TODO(jojo): We should get rid of "AllUserIds" and find a better way to
|
||||
// notify existing users the room has changed and they need to update it.
|
||||
AllUserIds []string `json:"alluserids,omitempty"`
|
||||
Properties *json.RawMessage `json:"properties,omitempty"`
|
||||
AllUserIds []string `json:"alluserids,omitempty"`
|
||||
Properties json.RawMessage `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomDisinviteRequest struct {
|
||||
|
@ -111,13 +127,13 @@ type BackendRoomDisinviteRequest struct {
|
|||
SessionIds []string `json:"sessionids,omitempty"`
|
||||
// TODO(jojo): We should get rid of "AllUserIds" and find a better way to
|
||||
// notify existing users the room has changed and they need to update it.
|
||||
AllUserIds []string `json:"alluserids,omitempty"`
|
||||
Properties *json.RawMessage `json:"properties,omitempty"`
|
||||
AllUserIds []string `json:"alluserids,omitempty"`
|
||||
Properties json.RawMessage `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomUpdateRequest struct {
|
||||
UserIds []string `json:"userids,omitempty"`
|
||||
Properties *json.RawMessage `json:"properties,omitempty"`
|
||||
UserIds []string `json:"userids,omitempty"`
|
||||
Properties json.RawMessage `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomDeleteRequest struct {
|
||||
|
@ -138,14 +154,91 @@ type BackendRoomParticipantsRequest struct {
|
|||
}
|
||||
|
||||
type BackendRoomMessageRequest struct {
|
||||
Data *json.RawMessage `json:"data,omitempty"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomSwitchToSessionsList []string
|
||||
type BackendRoomSwitchToSessionsMap map[string]json.RawMessage
|
||||
|
||||
type BackendRoomSwitchToMessageRequest struct {
|
||||
// Target room id
|
||||
RoomId string `json:"roomid"`
|
||||
|
||||
// Sessions is either a BackendRoomSwitchToSessionsList or a
|
||||
// BackendRoomSwitchToSessionsMap.
|
||||
// In the map, the key is the session id, the value additional details
|
||||
// (or null) for the session. The details will be included in the request
|
||||
// to the connected client.
|
||||
Sessions json.RawMessage `json:"sessions,omitempty"`
|
||||
|
||||
// Internal properties
|
||||
SessionsList BackendRoomSwitchToSessionsList `json:"sessionslist,omitempty"`
|
||||
SessionsMap BackendRoomSwitchToSessionsMap `json:"sessionsmap,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomDialoutRequest struct {
|
||||
// E.164 number to dial (e.g. "+1234567890")
|
||||
Number string `json:"number"`
|
||||
|
||||
Options json.RawMessage `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
checkE164Number = regexp.MustCompile(`^\+\d{2,}$`)
|
||||
)
|
||||
|
||||
func isValidNumber(s string) bool {
|
||||
return checkE164Number.MatchString(s)
|
||||
}
|
||||
|
||||
func (r *BackendRoomDialoutRequest) ValidateNumber() *Error {
|
||||
if r.Number == "" {
|
||||
return NewError("number_missing", "No number provided")
|
||||
}
|
||||
|
||||
if !isValidNumber(r.Number) {
|
||||
return NewError("invalid_number", "Expected E.164 number.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TransientAction string
|
||||
|
||||
const (
|
||||
TransientActionSet TransientAction = "set"
|
||||
TransientActionDelete TransientAction = "delete"
|
||||
)
|
||||
|
||||
type BackendRoomTransientRequest struct {
|
||||
Action TransientAction `json:"action"`
|
||||
Key string `json:"key"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
TTL time.Duration `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
type BackendServerRoomResponse struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
Dialout *BackendRoomDialoutResponse `json:"dialout,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomDialoutError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type BackendRoomDialoutResponse struct {
|
||||
CallId string `json:"callid,omitempty"`
|
||||
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Requests from the signaling server to the Nextcloud backend.
|
||||
|
||||
type BackendClientAuthRequest struct {
|
||||
Version string `json:"version"`
|
||||
Params *json.RawMessage `json:"params"`
|
||||
Version string `json:"version"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
type BackendClientRequest struct {
|
||||
|
@ -163,7 +256,7 @@ type BackendClientRequest struct {
|
|||
Session *BackendClientSessionRequest `json:"session,omitempty"`
|
||||
}
|
||||
|
||||
func NewBackendClientAuthRequest(params *json.RawMessage) *BackendClientRequest {
|
||||
func NewBackendClientAuthRequest(params json.RawMessage) *BackendClientRequest {
|
||||
return &BackendClientRequest{
|
||||
Type: "auth",
|
||||
Auth: &BackendClientAuthRequest{
|
||||
|
@ -191,9 +284,9 @@ type BackendClientResponse struct {
|
|||
}
|
||||
|
||||
type BackendClientAuthResponse struct {
|
||||
Version string `json:"version"`
|
||||
UserId string `json:"userid"`
|
||||
User *json.RawMessage `json:"user"`
|
||||
Version string `json:"version"`
|
||||
UserId string `json:"userid"`
|
||||
User json.RawMessage `json:"user"`
|
||||
}
|
||||
|
||||
type BackendClientRoomRequest struct {
|
||||
|
@ -222,14 +315,14 @@ func NewBackendClientRoomRequest(roomid string, userid string, sessionid string)
|
|||
}
|
||||
|
||||
type BackendClientRoomResponse struct {
|
||||
Version string `json:"version"`
|
||||
RoomId string `json:"roomid"`
|
||||
Properties *json.RawMessage `json:"properties"`
|
||||
Version string `json:"version"`
|
||||
RoomId string `json:"roomid"`
|
||||
Properties json.RawMessage `json:"properties"`
|
||||
|
||||
// Optional information about the Nextcloud Talk session. Can be used for
|
||||
// example to define a "userid" for otherwise anonymous users.
|
||||
// See "RoomSessionData" for a possible content.
|
||||
Session *json.RawMessage `json:"session,omitempty"`
|
||||
Session json.RawMessage `json:"session,omitempty"`
|
||||
|
||||
Permissions *[]Permission `json:"permissions,omitempty"`
|
||||
}
|
||||
|
@ -266,12 +359,12 @@ type BackendClientRingResponse struct {
|
|||
}
|
||||
|
||||
type BackendClientSessionRequest struct {
|
||||
Version string `json:"version"`
|
||||
RoomId string `json:"roomid"`
|
||||
Action string `json:"action"`
|
||||
SessionId string `json:"sessionid"`
|
||||
UserId string `json:"userid,omitempty"`
|
||||
User *json.RawMessage `json:"user,omitempty"`
|
||||
Version string `json:"version"`
|
||||
RoomId string `json:"roomid"`
|
||||
Action string `json:"action"`
|
||||
SessionId string `json:"sessionid"`
|
||||
UserId string `json:"userid,omitempty"`
|
||||
User json.RawMessage `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type BackendClientSessionResponse struct {
|
||||
|
@ -303,8 +396,8 @@ type OcsMeta struct {
|
|||
}
|
||||
|
||||
type OcsBody struct {
|
||||
Meta OcsMeta `json:"meta"`
|
||||
Data *json.RawMessage `json:"data"`
|
||||
Meta OcsMeta `json:"meta"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type OcsResponse struct {
|
||||
|
@ -321,3 +414,39 @@ type TurnCredentials struct {
|
|||
TTL int64 `json:"ttl"`
|
||||
URIs []string `json:"uris"`
|
||||
}
|
||||
|
||||
// Information on a backend in the etcd cluster.
|
||||
|
||||
type BackendInformationEtcd struct {
|
||||
parsedUrl *url.URL
|
||||
|
||||
Url string `json:"url"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
MaxStreamBitrate int `json:"maxstreambitrate,omitempty"`
|
||||
MaxScreenBitrate int `json:"maxscreenbitrate,omitempty"`
|
||||
|
||||
SessionLimit uint64 `json:"sessionlimit,omitempty"`
|
||||
}
|
||||
|
||||
func (p *BackendInformationEtcd) CheckValid() error {
|
||||
if p.Url == "" {
|
||||
return fmt.Errorf("url missing")
|
||||
}
|
||||
if p.Secret == "" {
|
||||
return fmt.Errorf("secret missing")
|
||||
}
|
||||
|
||||
parsedUrl, err := url.Parse(p.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid url: %w", err)
|
||||
}
|
||||
|
||||
if strings.Contains(parsedUrl.Host, ":") && hasStandardPort(parsedUrl) {
|
||||
parsedUrl.Host = parsedUrl.Hostname()
|
||||
p.Url = parsedUrl.String()
|
||||
}
|
||||
|
||||
p.parsedUrl = parsedUrl
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestBackendChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
rnd := newRandomString(32)
|
||||
body := []byte{1, 2, 3, 4, 5}
|
||||
secret := []byte("shared-secret")
|
||||
|
@ -56,3 +57,28 @@ func TestBackendChecksum(t *testing.T) {
|
|||
t.Errorf("Checksum %s could not be validated from request", check1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidNumbers(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid := []string{
|
||||
"+12",
|
||||
"+12345",
|
||||
}
|
||||
invalid := []string{
|
||||
"+1",
|
||||
"12345",
|
||||
" +12345",
|
||||
" +12345 ",
|
||||
"+123-45",
|
||||
}
|
||||
for _, number := range valid {
|
||||
if !isValidNumber(number) {
|
||||
t.Errorf("number %s should be valid", number)
|
||||
}
|
||||
}
|
||||
for _, number := range invalid {
|
||||
if isValidNumber(number) {
|
||||
t.Errorf("number %s should not be valid", number)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
41
api_grpc.go
Normal file
41
api_grpc.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* 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 (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Information on a GRPC target in the etcd cluster.
|
||||
|
||||
type GrpcTargetInformationEtcd struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
func (p *GrpcTargetInformationEtcd) CheckValid() error {
|
||||
if l := len(p.Address); l == 0 {
|
||||
return fmt.Errorf("address missing")
|
||||
} else if p.Address[l-1] == '/' {
|
||||
p.Address = p.Address[:l-1]
|
||||
}
|
||||
return nil
|
||||
}
|
91
api_proxy.go
91
api_proxy.go
|
@ -24,8 +24,9 @@ package signaling
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type ProxyClientMessage struct {
|
||||
|
@ -48,6 +49,14 @@ type ProxyClientMessage struct {
|
|||
Payload *PayloadProxyClientMessage `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ProxyClientMessage) String() string {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Could not serialize %#v: %s", m, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (m *ProxyClientMessage) CheckValid() error {
|
||||
switch m.Type {
|
||||
case "":
|
||||
|
@ -115,6 +124,14 @@ type ProxyServerMessage struct {
|
|||
Event *EventProxyServerMessage `json:"event,omitempty"`
|
||||
}
|
||||
|
||||
func (r *ProxyServerMessage) String() string {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Could not serialize %#v: %s", r, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (r *ProxyServerMessage) CloseAfterSend(session Session) bool {
|
||||
switch r.Type {
|
||||
case "bye":
|
||||
|
@ -127,7 +144,7 @@ func (r *ProxyServerMessage) CloseAfterSend(session Session) bool {
|
|||
// Type "hello"
|
||||
|
||||
type TokenClaims struct {
|
||||
jwt.StandardClaims
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type HelloProxyClientMessage struct {
|
||||
|
@ -142,7 +159,7 @@ type HelloProxyClientMessage struct {
|
|||
}
|
||||
|
||||
func (m *HelloProxyClientMessage) CheckValid() error {
|
||||
if m.Version != HelloVersion {
|
||||
if m.Version != HelloVersionV1 {
|
||||
return fmt.Errorf("unsupported hello version: %s", m.Version)
|
||||
}
|
||||
if m.ResumeId == "" {
|
||||
|
@ -156,8 +173,8 @@ func (m *HelloProxyClientMessage) CheckValid() error {
|
|||
type HelloProxyServerMessage struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
SessionId string `json:"sessionid"`
|
||||
Server *HelloServerMessageServer `json:"server,omitempty"`
|
||||
SessionId string `json:"sessionid"`
|
||||
Server *WelcomeServerMessage `json:"server,omitempty"`
|
||||
}
|
||||
|
||||
// Type "bye"
|
||||
|
@ -179,12 +196,20 @@ type ByeProxyServerMessage struct {
|
|||
type CommandProxyClientMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
Sid string `json:"sid,omitempty"`
|
||||
StreamType string `json:"streamType,omitempty"`
|
||||
PublisherId string `json:"publisherId,omitempty"`
|
||||
ClientId string `json:"clientId,omitempty"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
MediaTypes MediaType `json:"mediatypes,omitempty"`
|
||||
Sid string `json:"sid,omitempty"`
|
||||
StreamType StreamType `json:"streamType,omitempty"`
|
||||
PublisherId string `json:"publisherId,omitempty"`
|
||||
ClientId string `json:"clientId,omitempty"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
MediaTypes MediaType `json:"mediatypes,omitempty"`
|
||||
|
||||
RemoteUrl string `json:"remoteUrl,omitempty"`
|
||||
remoteUrl *url.URL
|
||||
RemoteToken string `json:"remoteToken,omitempty"`
|
||||
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
RtcpPort int `json:"rtcpPort,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CommandProxyClientMessage) CheckValid() error {
|
||||
|
@ -202,6 +227,17 @@ func (m *CommandProxyClientMessage) CheckValid() error {
|
|||
if m.StreamType == "" {
|
||||
return fmt.Errorf("stream type missing")
|
||||
}
|
||||
if m.RemoteUrl != "" {
|
||||
if m.RemoteToken == "" {
|
||||
return fmt.Errorf("remote token missing")
|
||||
}
|
||||
|
||||
remoteUrl, err := url.Parse(m.RemoteUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid remote url: %w", err)
|
||||
}
|
||||
m.remoteUrl = remoteUrl
|
||||
}
|
||||
case "delete-publisher":
|
||||
fallthrough
|
||||
case "delete-subscriber":
|
||||
|
@ -215,6 +251,10 @@ func (m *CommandProxyClientMessage) CheckValid() error {
|
|||
type CommandProxyServerMessage struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Sid string `json:"sid,omitempty"`
|
||||
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
|
||||
Streams []PublisherStream `json:"streams,omitempty"`
|
||||
}
|
||||
|
||||
// Type "payload"
|
||||
|
@ -259,12 +299,41 @@ type PayloadProxyServerMessage struct {
|
|||
|
||||
// Type "event"
|
||||
|
||||
type EventProxyServerBandwidth struct {
|
||||
// Incoming is the bandwidth utilization for publishers in percent.
|
||||
Incoming *float64 `json:"incoming,omitempty"`
|
||||
// Outgoing is the bandwidth utilization for subscribers in percent.
|
||||
Outgoing *float64 `json:"outgoing,omitempty"`
|
||||
}
|
||||
|
||||
func (b *EventProxyServerBandwidth) String() string {
|
||||
if b.Incoming != nil && b.Outgoing != nil {
|
||||
return fmt.Sprintf("bandwidth: incoming=%.3f%%, outgoing=%.3f%%", *b.Incoming, *b.Outgoing)
|
||||
} else if b.Incoming != nil {
|
||||
return fmt.Sprintf("bandwidth: incoming=%.3f%%, outgoing=unlimited", *b.Incoming)
|
||||
} else if b.Outgoing != nil {
|
||||
return fmt.Sprintf("bandwidth: incoming=unlimited, outgoing=%.3f%%", *b.Outgoing)
|
||||
} else {
|
||||
return "bandwidth: incoming=unlimited, outgoing=unlimited"
|
||||
}
|
||||
}
|
||||
|
||||
func (b EventProxyServerBandwidth) AllowIncoming() bool {
|
||||
return b.Incoming == nil || *b.Incoming < 100
|
||||
}
|
||||
|
||||
func (b EventProxyServerBandwidth) AllowOutgoing() bool {
|
||||
return b.Outgoing == nil || *b.Outgoing < 100
|
||||
}
|
||||
|
||||
type EventProxyServerMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
ClientId string `json:"clientId,omitempty"`
|
||||
Load int64 `json:"load,omitempty"`
|
||||
Sid string `json:"sid,omitempty"`
|
||||
|
||||
Bandwidth *EventProxyServerBandwidth `json:"bandwidth,omitempty"`
|
||||
}
|
||||
|
||||
// Information on a proxy in the etcd cluster.
|
||||
|
|
377
api_signaling.go
377
api_signaling.go
|
@ -23,14 +23,29 @@ package signaling
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/pion/sdp/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version that must be sent in a "hello" message.
|
||||
HelloVersion = "1.0"
|
||||
// Version 1.0 validates auth params against the Nextcloud instance.
|
||||
HelloVersionV1 = "1.0"
|
||||
|
||||
// Version 2.0 validates auth params encoded as JWT.
|
||||
HelloVersionV2 = "2.0"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSdp = NewError("no_sdp", "Payload does not contain a SDP.")
|
||||
ErrInvalidSdp = NewError("invalid_sdp", "Payload does not contain a valid SDP.")
|
||||
)
|
||||
|
||||
// ClientMessage is a message that is sent from a client to the server.
|
||||
|
@ -141,6 +156,8 @@ type ServerMessage struct {
|
|||
|
||||
Error *Error `json:"error,omitempty"`
|
||||
|
||||
Welcome *WelcomeServerMessage `json:"welcome,omitempty"`
|
||||
|
||||
Hello *HelloServerMessage `json:"hello,omitempty"`
|
||||
|
||||
Bye *ByeServerMessage `json:"bye,omitempty"`
|
||||
|
@ -154,6 +171,10 @@ type ServerMessage struct {
|
|||
Event *EventServerMessage `json:"event,omitempty"`
|
||||
|
||||
TransientData *TransientDataServerMessage `json:"transient,omitempty"`
|
||||
|
||||
Internal *InternalServerMessage `json:"internal,omitempty"`
|
||||
|
||||
Dialout *DialoutInternalClientMessage `json:"dialout,omitempty"`
|
||||
}
|
||||
|
||||
func (r *ServerMessage) CloseAfterSend(session Session) bool {
|
||||
|
@ -177,12 +198,12 @@ func (r *ServerMessage) CloseAfterSend(session Session) bool {
|
|||
}
|
||||
|
||||
func (r *ServerMessage) IsChatRefresh() bool {
|
||||
if r.Type != "message" || r.Message == nil || r.Message.Data == nil || len(*r.Message.Data) == 0 {
|
||||
if r.Type != "message" || r.Message == nil || len(r.Message.Data) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var data MessageServerMessageData
|
||||
if err := json.Unmarshal(*r.Message.Data, &data); err != nil {
|
||||
if err := json.Unmarshal(r.Message.Data, &data); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -212,9 +233,9 @@ func (r *ServerMessage) String() string {
|
|||
}
|
||||
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details interface{} `json:"details,omitempty"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details json.RawMessage `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func NewError(code string, message string) *Error {
|
||||
|
@ -222,10 +243,19 @@ func NewError(code string, message string) *Error {
|
|||
}
|
||||
|
||||
func NewErrorDetail(code string, message string, details interface{}) *Error {
|
||||
var rawDetails json.RawMessage
|
||||
if details != nil {
|
||||
var err error
|
||||
if rawDetails, err = json.Marshal(details); err != nil {
|
||||
log.Printf("Could not marshal details %+v for error %s with %s: %s", details, code, message, err)
|
||||
return NewError("internal_error", "Could not marshal error details")
|
||||
}
|
||||
}
|
||||
|
||||
return &Error{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
Details: rawDetails,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,6 +263,54 @@ func (e *Error) Error() string {
|
|||
return e.Message
|
||||
}
|
||||
|
||||
type WelcomeServerMessage struct {
|
||||
Version string `json:"version"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
}
|
||||
|
||||
func NewWelcomeServerMessage(version string, feature ...string) *WelcomeServerMessage {
|
||||
message := &WelcomeServerMessage{
|
||||
Version: version,
|
||||
Features: feature,
|
||||
}
|
||||
if len(feature) > 0 {
|
||||
sort.Strings(message.Features)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func (m *WelcomeServerMessage) AddFeature(feature ...string) {
|
||||
newFeatures := make([]string, len(m.Features))
|
||||
copy(newFeatures, m.Features)
|
||||
for _, feat := range feature {
|
||||
found := false
|
||||
for _, f := range newFeatures {
|
||||
if f == feat {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
newFeatures = append(newFeatures, feat)
|
||||
}
|
||||
}
|
||||
sort.Strings(newFeatures)
|
||||
m.Features = newFeatures
|
||||
}
|
||||
|
||||
func (m *WelcomeServerMessage) RemoveFeature(feature ...string) {
|
||||
newFeatures := make([]string, len(m.Features))
|
||||
copy(newFeatures, m.Features)
|
||||
for _, feat := range feature {
|
||||
idx := sort.SearchStrings(newFeatures, feat)
|
||||
if idx < len(newFeatures) && newFeatures[idx] == feat {
|
||||
newFeatures = append(newFeatures[:idx], newFeatures[idx+1:]...)
|
||||
}
|
||||
}
|
||||
m.Features = newFeatures
|
||||
}
|
||||
|
||||
const (
|
||||
HelloClientTypeClient = "client"
|
||||
HelloClientTypeInternal = "internal"
|
||||
|
@ -274,17 +352,35 @@ func (p *ClientTypeInternalAuthParams) CheckValid() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type HelloV2AuthParams struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (p *HelloV2AuthParams) CheckValid() error {
|
||||
if p.Token == "" {
|
||||
return fmt.Errorf("token missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type HelloV2TokenClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
|
||||
UserData json.RawMessage `json:"userdata,omitempty"`
|
||||
}
|
||||
|
||||
type HelloClientMessageAuth struct {
|
||||
// The client type that is connecting. Leave empty to use the default
|
||||
// "HelloClientTypeClient"
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
Params *json.RawMessage `json:"params"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
|
||||
Url string `json:"url"`
|
||||
parsedUrl *url.URL
|
||||
|
||||
internalParams ClientTypeInternalAuthParams
|
||||
helloV2Params HelloV2AuthParams
|
||||
}
|
||||
|
||||
// Type "hello"
|
||||
|
@ -297,15 +393,15 @@ type HelloClientMessage struct {
|
|||
Features []string `json:"features,omitempty"`
|
||||
|
||||
// The authentication credentials.
|
||||
Auth HelloClientMessageAuth `json:"auth"`
|
||||
Auth *HelloClientMessageAuth `json:"auth,omitempty"`
|
||||
}
|
||||
|
||||
func (m *HelloClientMessage) CheckValid() error {
|
||||
if m.Version != HelloVersion {
|
||||
return fmt.Errorf("unsupported hello version: %s", m.Version)
|
||||
if m.Version != HelloVersionV1 && m.Version != HelloVersionV2 {
|
||||
return InvalidHelloVersion
|
||||
}
|
||||
if m.ResumeId == "" {
|
||||
if m.Auth.Params == nil || len(*m.Auth.Params) == 0 {
|
||||
if m.Auth == nil || len(m.Auth.Params) == 0 {
|
||||
return fmt.Errorf("params missing")
|
||||
}
|
||||
if m.Auth.Type == "" {
|
||||
|
@ -324,8 +420,19 @@ func (m *HelloClientMessage) CheckValid() error {
|
|||
|
||||
m.Auth.parsedUrl = u
|
||||
}
|
||||
|
||||
switch m.Version {
|
||||
case HelloVersionV1:
|
||||
// No additional validation necessary.
|
||||
case HelloVersionV2:
|
||||
if err := json.Unmarshal(m.Auth.Params, &m.Auth.helloV2Params); err != nil {
|
||||
return err
|
||||
} else if err := m.Auth.helloV2Params.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case HelloClientTypeInternal:
|
||||
if err := json.Unmarshal(*m.Auth.Params, &m.Auth.internalParams); err != nil {
|
||||
if err := json.Unmarshal(m.Auth.Params, &m.Auth.internalParams); err != nil {
|
||||
return err
|
||||
} else if err := m.Auth.internalParams.CheckValid(); err != nil {
|
||||
return err
|
||||
|
@ -338,16 +445,24 @@ func (m *HelloClientMessage) CheckValid() error {
|
|||
}
|
||||
|
||||
const (
|
||||
// Features for all clients.
|
||||
// Features to send to all clients.
|
||||
ServerFeatureMcu = "mcu"
|
||||
ServerFeatureSimulcast = "simulcast"
|
||||
ServerFeatureUpdateSdp = "update-sdp"
|
||||
ServerFeatureAudioVideoPermissions = "audio-video-permissions"
|
||||
ServerFeatureTransientData = "transient-data"
|
||||
ServerFeatureInCallAll = "incall-all"
|
||||
ServerFeatureWelcome = "welcome"
|
||||
ServerFeatureHelloV2 = "hello-v2"
|
||||
ServerFeatureSwitchTo = "switchto"
|
||||
ServerFeatureDialout = "dialout"
|
||||
|
||||
// Features for internal clients only.
|
||||
// Features to send to internal clients only.
|
||||
ServerFeatureInternalVirtualSessions = "virtual-sessions"
|
||||
|
||||
// Possible client features from the "hello" request.
|
||||
ClientFeatureInternalInCall = "internal-incall"
|
||||
ClientFeatureStartDialout = "start-dialout"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -355,27 +470,41 @@ var (
|
|||
ServerFeatureAudioVideoPermissions,
|
||||
ServerFeatureTransientData,
|
||||
ServerFeatureInCallAll,
|
||||
ServerFeatureWelcome,
|
||||
ServerFeatureHelloV2,
|
||||
ServerFeatureSwitchTo,
|
||||
ServerFeatureDialout,
|
||||
}
|
||||
DefaultFeaturesInternal = []string{
|
||||
ServerFeatureInternalVirtualSessions,
|
||||
ServerFeatureTransientData,
|
||||
ServerFeatureInCallAll,
|
||||
ServerFeatureWelcome,
|
||||
ServerFeatureHelloV2,
|
||||
ServerFeatureSwitchTo,
|
||||
ServerFeatureDialout,
|
||||
}
|
||||
DefaultWelcomeFeatures = []string{
|
||||
ServerFeatureAudioVideoPermissions,
|
||||
ServerFeatureInternalVirtualSessions,
|
||||
ServerFeatureTransientData,
|
||||
ServerFeatureInCallAll,
|
||||
ServerFeatureWelcome,
|
||||
ServerFeatureHelloV2,
|
||||
ServerFeatureSwitchTo,
|
||||
ServerFeatureDialout,
|
||||
}
|
||||
)
|
||||
|
||||
type HelloServerMessageServer struct {
|
||||
Version string `json:"version"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
}
|
||||
|
||||
type HelloServerMessage struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
SessionId string `json:"sessionid"`
|
||||
ResumeId string `json:"resumeid"`
|
||||
UserId string `json:"userid"`
|
||||
Server *HelloServerMessageServer `json:"server,omitempty"`
|
||||
SessionId string `json:"sessionid"`
|
||||
ResumeId string `json:"resumeid"`
|
||||
UserId string `json:"userid"`
|
||||
|
||||
// TODO: Remove once all clients have switched to the "welcome" message.
|
||||
Server *WelcomeServerMessage `json:"server,omitempty"`
|
||||
}
|
||||
|
||||
// Type "bye"
|
||||
|
@ -405,8 +534,12 @@ func (m *RoomClientMessage) CheckValid() error {
|
|||
}
|
||||
|
||||
type RoomServerMessage struct {
|
||||
RoomId string `json:"roomid"`
|
||||
Properties *json.RawMessage `json:"properties,omitempty"`
|
||||
RoomId string `json:"roomid"`
|
||||
Properties json.RawMessage `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type RoomErrorDetails struct {
|
||||
Room *RoomServerMessage `json:"room"`
|
||||
}
|
||||
|
||||
// Type "message"
|
||||
|
@ -427,7 +560,7 @@ type MessageClientMessageRecipient struct {
|
|||
type MessageClientMessage struct {
|
||||
Recipient MessageClientMessageRecipient `json:"recipient"`
|
||||
|
||||
Data *json.RawMessage `json:"data"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type MessageClientMessageData struct {
|
||||
|
@ -436,10 +569,44 @@ type MessageClientMessageData struct {
|
|||
RoomType string `json:"roomType"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
|
||||
offerSdp *sdp.SessionDescription // Only set if Type == "offer"
|
||||
answerSdp *sdp.SessionDescription // Only set if Type == "answer"
|
||||
}
|
||||
|
||||
func (m *MessageClientMessageData) CheckValid() error {
|
||||
if m.RoomType != "" && !IsValidStreamType(m.RoomType) {
|
||||
return fmt.Errorf("invalid room type: %s", m.RoomType)
|
||||
}
|
||||
if m.Type == "offer" || m.Type == "answer" {
|
||||
sdpValue, found := m.Payload["sdp"]
|
||||
if !found {
|
||||
return ErrNoSdp
|
||||
}
|
||||
sdpText, ok := sdpValue.(string)
|
||||
if !ok {
|
||||
return ErrInvalidSdp
|
||||
}
|
||||
|
||||
var sdp sdp.SessionDescription
|
||||
if err := sdp.Unmarshal([]byte(sdpText)); err != nil {
|
||||
return NewErrorDetail("invalid_sdp", "Error parsing SDP from payload.", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
switch m.Type {
|
||||
case "offer":
|
||||
m.offerSdp = &sdp
|
||||
case "answer":
|
||||
m.answerSdp = &sdp
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MessageClientMessage) CheckValid() error {
|
||||
if m.Data == nil || len(*m.Data) == 0 {
|
||||
if len(m.Data) == 0 {
|
||||
return fmt.Errorf("message empty")
|
||||
}
|
||||
switch m.Recipient.Type {
|
||||
|
@ -480,7 +647,7 @@ type MessageServerMessage struct {
|
|||
Sender *MessageServerMessageSender `json:"sender"`
|
||||
Recipient *MessageClientMessageRecipient `json:"recipient,omitempty"`
|
||||
|
||||
Data *json.RawMessage `json:"data"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// Type "control"
|
||||
|
@ -497,7 +664,7 @@ type ControlServerMessage struct {
|
|||
Sender *MessageServerMessageSender `json:"sender"`
|
||||
Recipient *MessageClientMessageRecipient `json:"recipient,omitempty"`
|
||||
|
||||
Data *json.RawMessage `json:"data"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// Type "internal"
|
||||
|
@ -526,9 +693,10 @@ type AddSessionOptions struct {
|
|||
type AddSessionInternalClientMessage struct {
|
||||
CommonSessionInternalClientMessage
|
||||
|
||||
UserId string `json:"userid,omitempty"`
|
||||
User *json.RawMessage `json:"user,omitempty"`
|
||||
Flags uint32 `json:"flags,omitempty"`
|
||||
UserId string `json:"userid,omitempty"`
|
||||
User json.RawMessage `json:"user,omitempty"`
|
||||
Flags uint32 `json:"flags,omitempty"`
|
||||
InCall *int `json:"incall,omitempty"`
|
||||
|
||||
Options *AddSessionOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
@ -540,7 +708,8 @@ func (m *AddSessionInternalClientMessage) CheckValid() error {
|
|||
type UpdateSessionInternalClientMessage struct {
|
||||
CommonSessionInternalClientMessage
|
||||
|
||||
Flags *uint32 `json:"flags,omitempty"`
|
||||
Flags *uint32 `json:"flags,omitempty"`
|
||||
InCall *int `json:"incall,omitempty"`
|
||||
}
|
||||
|
||||
func (m *UpdateSessionInternalClientMessage) CheckValid() error {
|
||||
|
@ -557,6 +726,60 @@ func (m *RemoveSessionInternalClientMessage) CheckValid() error {
|
|||
return m.CommonSessionInternalClientMessage.CheckValid()
|
||||
}
|
||||
|
||||
type InCallInternalClientMessage struct {
|
||||
InCall int `json:"incall"`
|
||||
}
|
||||
|
||||
func (m *InCallInternalClientMessage) CheckValid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type DialoutStatus string
|
||||
|
||||
var (
|
||||
DialoutStatusAccepted DialoutStatus = "accepted"
|
||||
DialoutStatusRinging DialoutStatus = "ringing"
|
||||
DialoutStatusConnected DialoutStatus = "connected"
|
||||
DialoutStatusRejected DialoutStatus = "rejected"
|
||||
DialoutStatusCleared DialoutStatus = "cleared"
|
||||
)
|
||||
|
||||
type DialoutStatusInternalClientMessage struct {
|
||||
CallId string `json:"callid"`
|
||||
Status DialoutStatus `json:"status"`
|
||||
|
||||
// Cause is set if Status is "cleared" or "rejected".
|
||||
Cause string `json:"cause,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type DialoutInternalClientMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
RoomId string `json:"roomid,omitempty"`
|
||||
|
||||
Error *Error `json:"error,omitempty"`
|
||||
|
||||
Status *DialoutStatusInternalClientMessage `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DialoutInternalClientMessage) CheckValid() error {
|
||||
switch m.Type {
|
||||
case "":
|
||||
return errors.New("type missing")
|
||||
case "error":
|
||||
if m.Error == nil {
|
||||
return errors.New("error missing")
|
||||
}
|
||||
case "status":
|
||||
if m.Status == nil {
|
||||
return errors.New("status missing")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InternalClientMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
|
@ -565,10 +788,16 @@ type InternalClientMessage struct {
|
|||
UpdateSession *UpdateSessionInternalClientMessage `json:"updatesession,omitempty"`
|
||||
|
||||
RemoveSession *RemoveSessionInternalClientMessage `json:"removesession,omitempty"`
|
||||
|
||||
InCall *InCallInternalClientMessage `json:"incall,omitempty"`
|
||||
|
||||
Dialout *DialoutInternalClientMessage `json:"dialout,omitempty"`
|
||||
}
|
||||
|
||||
func (m *InternalClientMessage) CheckValid() error {
|
||||
switch m.Type {
|
||||
case "":
|
||||
return errors.New("type missing")
|
||||
case "addsession":
|
||||
if m.AddSession == nil {
|
||||
return fmt.Errorf("addsession missing")
|
||||
|
@ -587,23 +816,56 @@ func (m *InternalClientMessage) CheckValid() error {
|
|||
} else if err := m.RemoveSession.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "incall":
|
||||
if m.InCall == nil {
|
||||
return fmt.Errorf("incall missing")
|
||||
} else if err := m.InCall.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "dialout":
|
||||
if m.Dialout == nil {
|
||||
return fmt.Errorf("dialout missing")
|
||||
} else if err := m.Dialout.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InternalServerDialoutRequest struct {
|
||||
RoomId string `json:"roomid"`
|
||||
Backend string `json:"backend"`
|
||||
|
||||
Request *BackendRoomDialoutRequest `json:"request"`
|
||||
}
|
||||
|
||||
type InternalServerMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
Dialout *InternalServerDialoutRequest `json:"dialout,omitempty"`
|
||||
}
|
||||
|
||||
// Type "event"
|
||||
|
||||
type RoomEventServerMessage struct {
|
||||
RoomId string `json:"roomid"`
|
||||
Properties *json.RawMessage `json:"properties,omitempty"`
|
||||
RoomId string `json:"roomid"`
|
||||
Properties json.RawMessage `json:"properties,omitempty"`
|
||||
// TODO(jojo): Change "InCall" to "int" when #914 has landed in NC Talk.
|
||||
InCall *json.RawMessage `json:"incall,omitempty"`
|
||||
InCall json.RawMessage `json:"incall,omitempty"`
|
||||
Changed []map[string]interface{} `json:"changed,omitempty"`
|
||||
Users []map[string]interface{} `json:"users,omitempty"`
|
||||
|
||||
All bool `json:"all,omitempty"`
|
||||
}
|
||||
|
||||
func (m *RoomEventServerMessage) String() string {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Could not serialize %#v: %s", m, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
const (
|
||||
DisinviteReasonDisinvited = "disinvited"
|
||||
DisinviteReasonDeleted = "deleted"
|
||||
|
@ -616,8 +878,8 @@ type RoomDisinviteEventServerMessage struct {
|
|||
}
|
||||
|
||||
type RoomEventMessage struct {
|
||||
RoomId string `json:"roomid"`
|
||||
Data *json.RawMessage `json:"data,omitempty"`
|
||||
RoomId string `json:"roomid"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type RoomFlagsServerMessage struct {
|
||||
|
@ -643,9 +905,10 @@ type EventServerMessage struct {
|
|||
Type string `json:"type"`
|
||||
|
||||
// Used for target "room"
|
||||
Join []*EventServerMessageSessionEntry `json:"join,omitempty"`
|
||||
Leave []string `json:"leave,omitempty"`
|
||||
Change []*EventServerMessageSessionEntry `json:"change,omitempty"`
|
||||
Join []*EventServerMessageSessionEntry `json:"join,omitempty"`
|
||||
Leave []string `json:"leave,omitempty"`
|
||||
Change []*EventServerMessageSessionEntry `json:"change,omitempty"`
|
||||
SwitchTo *EventServerMessageSwitchTo `json:"switchto,omitempty"`
|
||||
|
||||
// Used for target "roomlist" / "participants"
|
||||
Invite *RoomEventServerMessage `json:"invite,omitempty"`
|
||||
|
@ -657,11 +920,19 @@ type EventServerMessage struct {
|
|||
Message *RoomEventMessage `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (m *EventServerMessage) String() string {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Could not serialize %#v: %s", m, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type EventServerMessageSessionEntry struct {
|
||||
SessionId string `json:"sessionid"`
|
||||
UserId string `json:"userid"`
|
||||
User *json.RawMessage `json:"user,omitempty"`
|
||||
RoomSessionId string `json:"roomsessionid,omitempty"`
|
||||
SessionId string `json:"sessionid"`
|
||||
UserId string `json:"userid"`
|
||||
User json.RawMessage `json:"user,omitempty"`
|
||||
RoomSessionId string `json:"roomsessionid,omitempty"`
|
||||
}
|
||||
|
||||
func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry {
|
||||
|
@ -673,6 +944,11 @@ func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry
|
|||
}
|
||||
}
|
||||
|
||||
type EventServerMessageSwitchTo struct {
|
||||
RoomId string `json:"roomid"`
|
||||
Details json.RawMessage `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// MCU-related types
|
||||
|
||||
type AnswerOfferMessage struct {
|
||||
|
@ -689,8 +965,9 @@ type AnswerOfferMessage struct {
|
|||
type TransientDataClientMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
Key string `json:"key,omitempty"`
|
||||
Value *json.RawMessage `json:"value,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Value json.RawMessage `json:"value,omitempty"`
|
||||
TTL time.Duration `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TransientDataClientMessage) CheckValid() error {
|
||||
|
|
|
@ -24,6 +24,8 @@ package signaling
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -79,6 +81,7 @@ func testMessages(t *testing.T, messageType string, valid_messages []testCheckVa
|
|||
}
|
||||
|
||||
func TestClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The message needs a type.
|
||||
msg := ClientMessage{}
|
||||
if err := msg.CheckValid(); err == nil {
|
||||
|
@ -87,77 +90,135 @@ func TestClientMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
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: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "client",
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Params: json.RawMessage("{}"),
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "internal",
|
||||
Params: (*json.RawMessage)(&internalAuthParams),
|
||||
Params: internalAuthParams,
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
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: HelloVersion},
|
||||
&HelloClientMessage{Version: HelloVersionV1},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
Type: "invalid-type",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Url: "https://domain.invalid",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Params: json.RawMessage("{}"),
|
||||
Url: "invalid-url",
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "internal",
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Params: json.RawMessage("{}"),
|
||||
},
|
||||
},
|
||||
&HelloClientMessage{
|
||||
Version: HelloVersion,
|
||||
Auth: HelloClientMessageAuth{
|
||||
Version: HelloVersionV1,
|
||||
Auth: &HelloClientMessageAuth{
|
||||
Type: "internal",
|
||||
Params: &json.RawMessage{'x', 'y', 'z'}, // Invalid JSON.
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -174,26 +235,27 @@ func TestHelloClientMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMessageClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid_messages := []testCheckValid{
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
SessionId: "the-session-id",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "user",
|
||||
UserId: "the-user-id",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "room",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
}
|
||||
invalid_messages := []testCheckValid{
|
||||
|
@ -208,20 +270,20 @@ func TestMessageClientMessage(t *testing.T) {
|
|||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "session",
|
||||
UserId: "the-user-id",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "user",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
|
@ -234,13 +296,13 @@ func TestMessageClientMessage(t *testing.T) {
|
|||
Type: "user",
|
||||
SessionId: "the-user-id",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
&MessageClientMessage{
|
||||
Recipient: MessageClientMessageRecipient{
|
||||
Type: "unknown-type",
|
||||
},
|
||||
Data: &json.RawMessage{'{', '}'},
|
||||
Data: json.RawMessage("{}"),
|
||||
},
|
||||
}
|
||||
testMessages(t, "message", valid_messages, invalid_messages)
|
||||
|
@ -255,6 +317,7 @@ func TestMessageClientMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestByeClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Any "bye" message is valid.
|
||||
valid_messages := []testCheckValid{
|
||||
&ByeClientMessage{},
|
||||
|
@ -273,6 +336,7 @@ func TestByeClientMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRoomClientMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Any "room" message is valid.
|
||||
valid_messages := []testCheckValid{
|
||||
&RoomClientMessage{},
|
||||
|
@ -291,6 +355,7 @@ func TestRoomClientMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestErrorMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
id := "request-id"
|
||||
msg := ClientMessage{
|
||||
Id: id,
|
||||
|
@ -323,12 +388,13 @@ func TestErrorMessages(t *testing.T) {
|
|||
}
|
||||
|
||||
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: (*json.RawMessage)(&data_true),
|
||||
Data: data_true,
|
||||
},
|
||||
}
|
||||
if !msg.IsChatRefresh() {
|
||||
|
@ -339,10 +405,50 @@ func TestIsChatRefresh(t *testing.T) {
|
|||
msg = ServerMessage{
|
||||
Type: "message",
|
||||
Message: &MessageServerMessage{
|
||||
Data: (*json.RawMessage)(&data_false),
|
||||
Data: data_false,
|
||||
},
|
||||
}
|
||||
if msg.IsChatRefresh() {
|
||||
t.Error("message should not be detected as chat refresh")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, result) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Welcome_AddRemoveFeature(t *testing.T) {
|
||||
t.Parallel()
|
||||
var msg WelcomeServerMessage
|
||||
assertEqualStrings(t, []string{}, msg.Features)
|
||||
|
||||
msg.AddFeature("one", "two", "one")
|
||||
assertEqualStrings(t, []string{"one", "two"}, msg.Features)
|
||||
if !sort.StringsAreSorted(msg.Features) {
|
||||
t.Errorf("features should be sorted, got %+v", msg.Features)
|
||||
}
|
||||
|
||||
msg.AddFeature("three")
|
||||
assertEqualStrings(t, []string{"one", "two", "three"}, msg.Features)
|
||||
if !sort.StringsAreSorted(msg.Features) {
|
||||
t.Errorf("features should be sorted, got %+v", msg.Features)
|
||||
}
|
||||
|
||||
msg.RemoveFeature("three", "one")
|
||||
assertEqualStrings(t, []string{"two"}, msg.Features)
|
||||
}
|
||||
|
|
210
async_events.go
Normal file
210
async_events.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* 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
|
||||
}
|
452
async_events_nats.go
Normal file
452
async_events_nats.go
Normal file
|
@ -0,0 +1,452 @@
|
|||
/**
|
||||
* 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)
|
||||
}
|
73
async_events_test.go
Normal file
73
async_events_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* 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"
|
||||
)
|
||||
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func getLoopbackAsyncEventsForTest(t *testing.T) AsyncEvents {
|
||||
events, err := NewAsyncEvents(NatsLoopbackUrl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
nats := (events.(*asyncEventsNats)).client
|
||||
(nats).(*LoopbackNatsClient).waitForSubscriptionsEmpty(ctx, t)
|
||||
})
|
||||
return events
|
||||
}
|
|
@ -39,6 +39,9 @@ import (
|
|||
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 {
|
||||
|
@ -50,8 +53,8 @@ type BackendClient struct {
|
|||
capabilities *Capabilities
|
||||
}
|
||||
|
||||
func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost int, version string) (*BackendClient, error) {
|
||||
backends, err := NewBackendConfiguration(config)
|
||||
func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost int, version string, etcdClient *EtcdClient) (*BackendClient, error) {
|
||||
backends, err := NewBackendConfiguration(config, etcdClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -80,6 +83,10 @@ func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost in
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (b *BackendClient) Close() {
|
||||
b.backends.Close()
|
||||
}
|
||||
|
||||
func (b *BackendClient) Reload(config *goconf.ConfigFile) {
|
||||
b.backends.Reload(config)
|
||||
}
|
||||
|
@ -187,11 +194,19 @@ func (b *BackendClient) PerformJSONRequest(ctx context.Context, u *url.URL, requ
|
|||
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 || ocs.Ocs.Data == nil {
|
||||
} else if ocs.Ocs == nil || len(ocs.Ocs.Data) == 0 {
|
||||
log.Printf("Incomplete OCS response %s from %s", string(body), req.URL)
|
||||
return fmt.Errorf("incomplete OCS response")
|
||||
} else 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 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 {
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
|
@ -44,9 +45,17 @@ func returnOCS(t *testing.T, w http.ResponseWriter, body []byte) {
|
|||
StatusCode: http.StatusOK,
|
||||
Message: "OK",
|
||||
},
|
||||
Data: (*json.RawMessage)(&body),
|
||||
Data: body,
|
||||
},
|
||||
}
|
||||
if strings.Contains(t.Name(), "Throttled") {
|
||||
response.Ocs.Meta = OcsMeta{
|
||||
Status: "failure",
|
||||
StatusCode: 429,
|
||||
Message: "Reached maximum delay",
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -61,6 +70,8 @@ func returnOCS(t *testing.T, w http.ResponseWriter, body []byte) {
|
|||
}
|
||||
|
||||
func TestPostOnRedirect(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/ocs/v2.php/two", http.StatusTemporaryRedirect)
|
||||
|
@ -95,7 +106,7 @@ func TestPostOnRedirect(t *testing.T) {
|
|||
if u.Scheme == "http" {
|
||||
config.AddOption("backend", "allowhttp", "true")
|
||||
}
|
||||
client, err := NewBackendClient(config, 1, "0.0")
|
||||
client, err := NewBackendClient(config, 1, "0.0", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -116,6 +127,8 @@ func TestPostOnRedirect(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPostOnRedirectDifferentHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "http://domain.invalid/ocs/v2.php/two", http.StatusTemporaryRedirect)
|
||||
|
@ -134,7 +147,7 @@ func TestPostOnRedirectDifferentHost(t *testing.T) {
|
|||
if u.Scheme == "http" {
|
||||
config.AddOption("backend", "allowhttp", "true")
|
||||
}
|
||||
client, err := NewBackendClient(config, 1, "0.0")
|
||||
client, err := NewBackendClient(config, 1, "0.0", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -156,6 +169,8 @@ func TestPostOnRedirectDifferentHost(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPostOnRedirectStatusFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/ocs/v2.php/two", http.StatusFound)
|
||||
|
@ -187,7 +202,7 @@ func TestPostOnRedirectStatusFound(t *testing.T) {
|
|||
if u.Scheme == "http" {
|
||||
config.AddOption("backend", "allowhttp", "true")
|
||||
}
|
||||
client, err := NewBackendClient(config, 1, "0.0")
|
||||
client, err := NewBackendClient(config, 1, "0.0", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -206,3 +221,42 @@ func TestPostOnRedirectStatusFound(t *testing.T) {
|
|||
t.Errorf("Expected empty response, got %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleThrottled(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) {
|
||||
returnOCS(t, w, []byte("[]"))
|
||||
})
|
||||
server := httptest.NewServer(r)
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL + "/ocs/v2.php/one")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("backend", "allowed", u.Host)
|
||||
config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
if u.Scheme == "http" {
|
||||
config.AddOption("backend", "allowhttp", "true")
|
||||
}
|
||||
client, err := NewBackendClient(config, 1, "0.0", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
request := map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
var response map[string]string
|
||||
err = client.PerformJSONRequest(ctx, u, request, &response)
|
||||
if err == nil {
|
||||
t.Error("should have triggered an error")
|
||||
} else if !errors.Is(err, ErrThrottledResponse) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,24 +22,31 @@
|
|||
package signaling
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
)
|
||||
|
||||
const (
|
||||
BackendTypeStatic = "static"
|
||||
BackendTypeEtcd = "etcd"
|
||||
|
||||
DefaultBackendType = BackendTypeStatic
|
||||
)
|
||||
|
||||
var (
|
||||
SessionLimitExceeded = NewError("session_limit_exceeded", "Too many sessions connected for this backend.")
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
id string
|
||||
url string
|
||||
secret []byte
|
||||
compat bool
|
||||
id string
|
||||
url string
|
||||
parsedUrl *url.URL
|
||||
secret []byte
|
||||
compat bool
|
||||
|
||||
allowHttp bool
|
||||
|
||||
|
@ -74,6 +81,24 @@ func (b *Backend) IsUrlAllowed(u *url.URL) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *Backend) Url() string {
|
||||
return b.url
|
||||
}
|
||||
|
||||
func (b *Backend) ParsedUrl() *url.URL {
|
||||
return b.parsedUrl
|
||||
}
|
||||
|
||||
func (b *Backend) Limit() int {
|
||||
return int(b.sessionLimit)
|
||||
}
|
||||
|
||||
func (b *Backend) Len() int {
|
||||
b.sessionsLock.Lock()
|
||||
defer b.sessionsLock.Unlock()
|
||||
return len(b.sessions)
|
||||
}
|
||||
|
||||
func (b *Backend) AddSession(session Session) error {
|
||||
if session.ClientType() == HelloClientTypeInternal || session.ClientType() == HelloClientTypeVirtual {
|
||||
// Internal and virtual sessions are not counting to the limit.
|
||||
|
@ -105,271 +130,43 @@ func (b *Backend) RemoveSession(session Session) {
|
|||
delete(b.sessions, session.PublicId())
|
||||
}
|
||||
|
||||
type BackendConfiguration struct {
|
||||
type BackendStorage interface {
|
||||
Close()
|
||||
Reload(config *goconf.ConfigFile)
|
||||
|
||||
GetCompatBackend() *Backend
|
||||
GetBackend(u *url.URL) *Backend
|
||||
GetBackends() []*Backend
|
||||
}
|
||||
|
||||
type backendStorageCommon struct {
|
||||
mu sync.RWMutex
|
||||
backends map[string][]*Backend
|
||||
|
||||
// Deprecated
|
||||
allowAll bool
|
||||
commonSecret []byte
|
||||
compatBackend *Backend
|
||||
}
|
||||
|
||||
func NewBackendConfiguration(config *goconf.ConfigFile) (*BackendConfiguration, 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
|
||||
func (s *backendStorageCommon) GetBackends() []*Backend {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*Backend
|
||||
for _, entries := range s.backends {
|
||||
result = append(result, entries...)
|
||||
}
|
||||
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)
|
||||
}
|
||||
numBackends++
|
||||
} else if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" {
|
||||
for host, configuredBackends := range getConfiguredHosts(backendIds, config) {
|
||||
backends[host] = append(backends[host], configuredBackends...)
|
||||
for _, be := range configuredBackends {
|
||||
log.Printf("Backend %s added for %s", be.id, be.url)
|
||||
}
|
||||
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)
|
||||
}
|
||||
numBackends++
|
||||
}
|
||||
}
|
||||
|
||||
RegisterBackendConfigurationStats()
|
||||
statsBackendsCurrent.Add(float64(numBackends))
|
||||
|
||||
return &BackendConfiguration{
|
||||
backends: backends,
|
||||
|
||||
allowAll: allowAll,
|
||||
commonSecret: []byte(commonSecret),
|
||||
compatBackend: compatBackend,
|
||||
}, nil
|
||||
return result
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) RemoveBackendsForHost(host string) {
|
||||
if oldBackends := b.backends[host]; len(oldBackends) > 0 {
|
||||
for _, backend := range oldBackends {
|
||||
log.Printf("Backend %s removed for %s", backend.id, backend.url)
|
||||
}
|
||||
statsBackendsCurrent.Sub(float64(len(oldBackends)))
|
||||
}
|
||||
delete(b.backends, host)
|
||||
}
|
||||
func (s *backendStorageCommon) getBackendLocked(u *url.URL) *Backend {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
func (b *BackendConfiguration) UpsertHost(host string, backends []*Backend) {
|
||||
for existingIndex, existingBackend := range b.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
|
||||
b.backends[host][existingIndex] = newBackend
|
||||
backends = append(backends[:index], backends[index+1:]...)
|
||||
log.Printf("Backend %s updated for %s", newBackend.id, newBackend.url)
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
if !found {
|
||||
removed := b.backends[host][existingIndex]
|
||||
log.Printf("Backend %s removed for %s", removed.id, removed.url)
|
||||
b.backends[host] = append(b.backends[host][:existingIndex], b.backends[host][existingIndex+1:]...)
|
||||
statsBackendsCurrent.Dec()
|
||||
}
|
||||
}
|
||||
|
||||
b.backends[host] = append(b.backends[host], backends...)
|
||||
for _, added := range backends {
|
||||
log.Printf("Backend %s added for %s", added.id, added.url)
|
||||
}
|
||||
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) (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 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,
|
||||
secret: []byte(secret),
|
||||
|
||||
allowHttp: parsed.Scheme == "http",
|
||||
|
||||
maxStreamBitrate: maxStreamBitrate,
|
||||
maxScreenBitrate: maxScreenBitrate,
|
||||
|
||||
sessionLimit: uint64(sessionLimit),
|
||||
})
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) Reload(config *goconf.ConfigFile) {
|
||||
if b.compatBackend != nil {
|
||||
log.Println("Old-style configuration active, reload is not supported")
|
||||
return
|
||||
}
|
||||
|
||||
if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" {
|
||||
configuredHosts := getConfiguredHosts(backendIds, config)
|
||||
|
||||
// remove backends that are no longer configured
|
||||
for hostname := range b.backends {
|
||||
if _, ok := configuredHosts[hostname]; !ok {
|
||||
b.RemoveBackendsForHost(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// rewrite backends adding newly configured ones and rewriting existing ones
|
||||
for hostname, configuredBackends := range configuredHosts {
|
||||
b.UpsertHost(hostname, configuredBackends)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) GetCompatBackend() *Backend {
|
||||
return b.compatBackend
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) GetBackend(u *url.URL) *Backend {
|
||||
if strings.Contains(u.Host, ":") && hasStandardPort(u) {
|
||||
u.Host = u.Hostname()
|
||||
}
|
||||
|
||||
entries, found := b.backends[u.Host]
|
||||
entries, found := s.backends[u.Host]
|
||||
if !found {
|
||||
if b.allowAll {
|
||||
return b.compatBackend
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
s := u.String()
|
||||
if s[len(s)-1] != '/' {
|
||||
s += "/"
|
||||
url := u.String()
|
||||
if url[len(url)-1] != '/' {
|
||||
url += "/"
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsUrlAllowed(u) {
|
||||
|
@ -379,7 +176,7 @@ func (b *BackendConfiguration) GetBackend(u *url.URL) *Backend {
|
|||
if entry.url == "" {
|
||||
// Old-style configuration, only hosts are configured.
|
||||
return entry
|
||||
} else if strings.HasPrefix(s, entry.url) {
|
||||
} else if strings.HasPrefix(url, entry.url) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
@ -387,12 +184,59 @@ func (b *BackendConfiguration) GetBackend(u *url.URL) *Backend {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) GetBackends() []*Backend {
|
||||
var result []*Backend
|
||||
for _, entries := range b.backends {
|
||||
result = append(result, entries...)
|
||||
type BackendConfiguration struct {
|
||||
storage BackendStorage
|
||||
}
|
||||
|
||||
func NewBackendConfiguration(config *goconf.ConfigFile, etcdClient *EtcdClient) (*BackendConfiguration, error) {
|
||||
backendType, _ := config.GetString("backend", "backendtype")
|
||||
if backendType == "" {
|
||||
backendType = DefaultBackendType
|
||||
}
|
||||
return result
|
||||
|
||||
RegisterBackendConfigurationStats()
|
||||
|
||||
var storage BackendStorage
|
||||
var err error
|
||||
switch backendType {
|
||||
case BackendTypeStatic:
|
||||
storage, err = NewBackendStorageStatic(config)
|
||||
case BackendTypeEtcd:
|
||||
storage, err = NewBackendStorageEtcd(config, etcdClient)
|
||||
default:
|
||||
err = fmt.Errorf("unknown backend type: %s", backendType)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BackendConfiguration{
|
||||
storage: storage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) Close() {
|
||||
b.storage.Close()
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) Reload(config *goconf.ConfigFile) {
|
||||
b.storage.Reload(config)
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) GetCompatBackend() *Backend {
|
||||
return b.storage.GetCompatBackend()
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) GetBackend(u *url.URL) *Backend {
|
||||
if strings.Contains(u.Host, ":") && hasStandardPort(u) {
|
||||
u.Host = u.Hostname()
|
||||
}
|
||||
|
||||
return b.storage.GetBackend(u)
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) GetBackends() []*Backend {
|
||||
return b.storage.GetBackends()
|
||||
}
|
||||
|
||||
func (b *BackendConfiguration) IsUrlAllowed(u *url.URL) bool {
|
||||
|
@ -416,5 +260,5 @@ func (b *BackendConfiguration) GetSecret(u *url.URL) []byte {
|
|||
return nil
|
||||
}
|
||||
|
||||
return entry.secret
|
||||
return entry.Secret()
|
||||
}
|
||||
|
|
|
@ -23,8 +23,10 @@ package signaling
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
|
@ -90,6 +92,7 @@ func testBackends(t *testing.T, config *BackendConfiguration, valid_urls [][]str
|
|||
}
|
||||
|
||||
func TestIsUrlAllowed_Compat(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
// Old-style configuration
|
||||
valid_urls := []string{
|
||||
"http://domain.invalid",
|
||||
|
@ -104,7 +107,7 @@ func TestIsUrlAllowed_Compat(t *testing.T) {
|
|||
config.AddOption("backend", "allowed", "domain.invalid")
|
||||
config.AddOption("backend", "allowhttp", "true")
|
||||
config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
cfg, err := NewBackendConfiguration(config)
|
||||
cfg, err := NewBackendConfiguration(config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -112,6 +115,7 @@ func TestIsUrlAllowed_Compat(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsUrlAllowed_CompatForceHttps(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
// Old-style configuration, force HTTPS
|
||||
valid_urls := []string{
|
||||
"https://domain.invalid",
|
||||
|
@ -125,7 +129,7 @@ func TestIsUrlAllowed_CompatForceHttps(t *testing.T) {
|
|||
config := goconf.NewConfigFile()
|
||||
config.AddOption("backend", "allowed", "domain.invalid")
|
||||
config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
cfg, err := NewBackendConfiguration(config)
|
||||
cfg, err := NewBackendConfiguration(config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -133,6 +137,7 @@ func TestIsUrlAllowed_CompatForceHttps(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsUrlAllowed(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
valid_urls := [][]string{
|
||||
{"https://domain.invalid/foo", string(testBackendSecret) + "-foo"},
|
||||
{"https://domain.invalid/foo/", string(testBackendSecret) + "-foo"},
|
||||
|
@ -170,7 +175,7 @@ func TestIsUrlAllowed(t *testing.T) {
|
|||
config.AddOption("baz", "secret", string(testBackendSecret)+"-baz")
|
||||
config.AddOption("lala", "url", "https://otherdomain.invalid/")
|
||||
config.AddOption("lala", "secret", string(testBackendSecret)+"-lala")
|
||||
cfg, err := NewBackendConfiguration(config)
|
||||
cfg, err := NewBackendConfiguration(config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -178,6 +183,7 @@ func TestIsUrlAllowed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsUrlAllowed_EmptyAllowlist(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
valid_urls := []string{}
|
||||
invalid_urls := []string{
|
||||
"http://domain.invalid",
|
||||
|
@ -187,7 +193,7 @@ func TestIsUrlAllowed_EmptyAllowlist(t *testing.T) {
|
|||
config := goconf.NewConfigFile()
|
||||
config.AddOption("backend", "allowed", "")
|
||||
config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
cfg, err := NewBackendConfiguration(config)
|
||||
cfg, err := NewBackendConfiguration(config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -195,6 +201,7 @@ func TestIsUrlAllowed_EmptyAllowlist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsUrlAllowed_AllowAll(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
valid_urls := []string{
|
||||
"http://domain.invalid",
|
||||
"https://domain.invalid",
|
||||
|
@ -207,7 +214,7 @@ func TestIsUrlAllowed_AllowAll(t *testing.T) {
|
|||
config.AddOption("backend", "allowall", "true")
|
||||
config.AddOption("backend", "allowed", "")
|
||||
config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
cfg, err := NewBackendConfiguration(config)
|
||||
cfg, err := NewBackendConfiguration(config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -220,6 +227,7 @@ type ParseBackendIdsTestcase struct {
|
|||
}
|
||||
|
||||
func TestParseBackendIds(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
testcases := []ParseBackendIdsTestcase{
|
||||
{"", nil},
|
||||
{"backend1", []string{"backend1"}},
|
||||
|
@ -239,6 +247,7 @@ func TestParseBackendIds(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackendReloadNoChange(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
current := testutil.ToFloat64(statsBackendsCurrent)
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
|
@ -247,7 +256,7 @@ func TestBackendReloadNoChange(t *testing.T) {
|
|||
original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
original_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
o_cfg, err := NewBackendConfiguration(original_config)
|
||||
o_cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -260,7 +269,7 @@ func TestBackendReloadNoChange(t *testing.T) {
|
|||
new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
new_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
n_cfg, err := NewBackendConfiguration(new_config)
|
||||
n_cfg, err := NewBackendConfiguration(new_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -274,6 +283,7 @@ func TestBackendReloadNoChange(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackendReloadChangeExistingURL(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
current := testutil.ToFloat64(statsBackendsCurrent)
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
|
@ -282,7 +292,7 @@ func TestBackendReloadChangeExistingURL(t *testing.T) {
|
|||
original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
original_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
o_cfg, err := NewBackendConfiguration(original_config)
|
||||
o_cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -296,7 +306,7 @@ func TestBackendReloadChangeExistingURL(t *testing.T) {
|
|||
new_config.AddOption("backend1", "sessionlimit", "10")
|
||||
new_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
n_cfg, err := NewBackendConfiguration(new_config)
|
||||
n_cfg, err := NewBackendConfiguration(new_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -314,6 +324,7 @@ func TestBackendReloadChangeExistingURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackendReloadChangeSecret(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
current := testutil.ToFloat64(statsBackendsCurrent)
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
|
@ -322,7 +333,7 @@ func TestBackendReloadChangeSecret(t *testing.T) {
|
|||
original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
original_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
o_cfg, err := NewBackendConfiguration(original_config)
|
||||
o_cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -335,7 +346,7 @@ func TestBackendReloadChangeSecret(t *testing.T) {
|
|||
new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend3")
|
||||
new_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
n_cfg, err := NewBackendConfiguration(new_config)
|
||||
n_cfg, err := NewBackendConfiguration(new_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -352,13 +363,14 @@ func TestBackendReloadChangeSecret(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackendReloadAddBackend(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
current := testutil.ToFloat64(statsBackendsCurrent)
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1")
|
||||
original_config.AddOption("backend", "allowall", "false")
|
||||
original_config.AddOption("backend1", "url", "http://domain1.invalid")
|
||||
original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
o_cfg, err := NewBackendConfiguration(original_config)
|
||||
o_cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -372,7 +384,7 @@ func TestBackendReloadAddBackend(t *testing.T) {
|
|||
new_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
new_config.AddOption("backend2", "sessionlimit", "10")
|
||||
n_cfg, err := NewBackendConfiguration(new_config)
|
||||
n_cfg, err := NewBackendConfiguration(new_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -392,6 +404,7 @@ func TestBackendReloadAddBackend(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackendReloadRemoveHost(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
current := testutil.ToFloat64(statsBackendsCurrent)
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
|
@ -400,7 +413,7 @@ func TestBackendReloadRemoveHost(t *testing.T) {
|
|||
original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
original_config.AddOption("backend2", "url", "http://domain2.invalid")
|
||||
original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
o_cfg, err := NewBackendConfiguration(original_config)
|
||||
o_cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -411,7 +424,7 @@ func TestBackendReloadRemoveHost(t *testing.T) {
|
|||
new_config.AddOption("backend", "allowall", "false")
|
||||
new_config.AddOption("backend1", "url", "http://domain1.invalid")
|
||||
new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
n_cfg, err := NewBackendConfiguration(new_config)
|
||||
n_cfg, err := NewBackendConfiguration(new_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -429,6 +442,7 @@ func TestBackendReloadRemoveHost(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackendReloadRemoveBackendFromSharedHost(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
current := testutil.ToFloat64(statsBackendsCurrent)
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
|
@ -437,7 +451,7 @@ func TestBackendReloadRemoveBackendFromSharedHost(t *testing.T) {
|
|||
original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
original_config.AddOption("backend2", "url", "http://domain1.invalid/bar/")
|
||||
original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
o_cfg, err := NewBackendConfiguration(original_config)
|
||||
o_cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -448,7 +462,7 @@ func TestBackendReloadRemoveBackendFromSharedHost(t *testing.T) {
|
|||
new_config.AddOption("backend", "allowall", "false")
|
||||
new_config.AddOption("backend1", "url", "http://domain1.invalid/foo/")
|
||||
new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
n_cfg, err := NewBackendConfiguration(new_config)
|
||||
n_cfg, err := NewBackendConfiguration(new_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -464,3 +478,209 @@ func TestBackendReloadRemoveBackendFromSharedHost(t *testing.T) {
|
|||
t.Error("BackendConfiguration should be equal after Reload")
|
||||
}
|
||||
}
|
||||
|
||||
func sortBackends(backends []*Backend) []*Backend {
|
||||
result := make([]*Backend, len(backends))
|
||||
copy(result, backends)
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Id() < result[j].Id()
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func mustParse(s string) *url.URL {
|
||||
p, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestBackendConfiguration_Etcd(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
etcd, client := NewEtcdClientForTest(t)
|
||||
|
||||
url1 := "https://domain1.invalid/foo"
|
||||
initialSecret1 := string(testBackendSecret) + "-backend1-initial"
|
||||
secret1 := string(testBackendSecret) + "-backend1"
|
||||
|
||||
SetEtcdValue(etcd, "/backends/1_one", []byte("{\"url\":\""+url1+"\",\"secret\":\""+initialSecret1+"\"}"))
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("backend", "backendtype", "etcd")
|
||||
config.AddOption("backend", "backendprefix", "/backends")
|
||||
|
||||
cfg, err := NewBackendConfiguration(config, client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cfg.Close()
|
||||
|
||||
storage := cfg.storage.(*backendStorageEtcd)
|
||||
ch := storage.getWakeupChannelForTesting()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := storage.WaitForInitialized(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if backends := sortBackends(cfg.GetBackends()); len(backends) != 1 {
|
||||
t.Errorf("Expected one backend, got %+v", backends)
|
||||
} else if backends[0].url != url1 {
|
||||
t.Errorf("Expected backend url %s, got %s", url1, backends[0].url)
|
||||
} else if string(backends[0].secret) != initialSecret1 {
|
||||
t.Errorf("Expected backend secret %s, got %s", initialSecret1, string(backends[0].secret))
|
||||
} else if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[0], backend)
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
SetEtcdValue(etcd, "/backends/1_one", []byte("{\"url\":\""+url1+"\",\"secret\":\""+secret1+"\"}"))
|
||||
<-ch
|
||||
if backends := sortBackends(cfg.GetBackends()); len(backends) != 1 {
|
||||
t.Errorf("Expected one backend, got %+v", backends)
|
||||
} else if backends[0].url != url1 {
|
||||
t.Errorf("Expected backend url %s, got %s", url1, backends[0].url)
|
||||
} else if string(backends[0].secret) != secret1 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret1, string(backends[0].secret))
|
||||
} else if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[0], backend)
|
||||
}
|
||||
|
||||
url2 := "https://domain1.invalid/bar"
|
||||
secret2 := string(testBackendSecret) + "-backend2"
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
SetEtcdValue(etcd, "/backends/2_two", []byte("{\"url\":\""+url2+"\",\"secret\":\""+secret2+"\"}"))
|
||||
<-ch
|
||||
if backends := sortBackends(cfg.GetBackends()); len(backends) != 2 {
|
||||
t.Errorf("Expected two backends, got %+v", backends)
|
||||
} else if backends[0].url != url1 {
|
||||
t.Errorf("Expected backend url %s, got %s", url1, backends[0].url)
|
||||
} else if string(backends[0].secret) != secret1 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret1, string(backends[0].secret))
|
||||
} else if backends[1].url != url2 {
|
||||
t.Errorf("Expected backend url %s, got %s", url2, backends[1].url)
|
||||
} else if string(backends[1].secret) != secret2 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret2, string(backends[1].secret))
|
||||
} else if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[0], backend)
|
||||
} else if backend := cfg.GetBackend(mustParse(url2)); backend != backends[1] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[1], backend)
|
||||
}
|
||||
|
||||
url3 := "https://domain2.invalid/foo"
|
||||
secret3 := string(testBackendSecret) + "-backend3"
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
SetEtcdValue(etcd, "/backends/3_three", []byte("{\"url\":\""+url3+"\",\"secret\":\""+secret3+"\"}"))
|
||||
<-ch
|
||||
if backends := sortBackends(cfg.GetBackends()); len(backends) != 3 {
|
||||
t.Errorf("Expected three backends, got %+v", backends)
|
||||
} else if backends[0].url != url1 {
|
||||
t.Errorf("Expected backend url %s, got %s", url1, backends[0].url)
|
||||
} else if string(backends[0].secret) != secret1 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret1, string(backends[0].secret))
|
||||
} else if backends[1].url != url2 {
|
||||
t.Errorf("Expected backend url %s, got %s", url2, backends[1].url)
|
||||
} else if string(backends[1].secret) != secret2 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret2, string(backends[1].secret))
|
||||
} else if backends[2].url != url3 {
|
||||
t.Errorf("Expected backend url %s, got %s", url3, backends[2].url)
|
||||
} else if string(backends[2].secret) != secret3 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret3, string(backends[2].secret))
|
||||
} else if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[0], backend)
|
||||
} else if backend := cfg.GetBackend(mustParse(url2)); backend != backends[1] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[1], backend)
|
||||
} else if backend := cfg.GetBackend(mustParse(url3)); backend != backends[2] {
|
||||
t.Errorf("Expected backend %+v, got %+v", backends[2], backend)
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
DeleteEtcdValue(etcd, "/backends/1_one")
|
||||
<-ch
|
||||
if backends := sortBackends(cfg.GetBackends()); len(backends) != 2 {
|
||||
t.Errorf("Expected two backends, got %+v", backends)
|
||||
} else if backends[0].url != url2 {
|
||||
t.Errorf("Expected backend url %s, got %s", url2, backends[0].url)
|
||||
} else if string(backends[0].secret) != secret2 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret2, string(backends[0].secret))
|
||||
} else if backends[1].url != url3 {
|
||||
t.Errorf("Expected backend url %s, got %s", url3, backends[1].url)
|
||||
} else if string(backends[1].secret) != secret3 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret3, string(backends[1].secret))
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
DeleteEtcdValue(etcd, "/backends/2_two")
|
||||
<-ch
|
||||
if backends := sortBackends(cfg.GetBackends()); len(backends) != 1 {
|
||||
t.Errorf("Expected one backend, got %+v", backends)
|
||||
} else if backends[0].url != url3 {
|
||||
t.Errorf("Expected backend url %s, got %s", url3, backends[0].url)
|
||||
} else if string(backends[0].secret) != secret3 {
|
||||
t.Errorf("Expected backend secret %s, got %s", secret3, string(backends[0].secret))
|
||||
}
|
||||
|
||||
if _, found := storage.backends["domain1.invalid"]; found {
|
||||
t.Errorf("Should have removed host information for %s", "domain1.invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendCommonSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
u1, err := url.Parse("http://domain1.invalid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u2, err := url.Parse("http://domain2.invalid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
original_config := goconf.NewConfigFile()
|
||||
original_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
original_config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
original_config.AddOption("backend1", "url", u1.String())
|
||||
original_config.AddOption("backend2", "url", u2.String())
|
||||
original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2")
|
||||
cfg, err := NewBackendConfiguration(original_config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b1 := cfg.GetBackend(u1); b1 == nil {
|
||||
t.Error("didn't get backend")
|
||||
} else if !bytes.Equal(b1.Secret(), testBackendSecret) {
|
||||
t.Errorf("expected secret %s, got %s", string(testBackendSecret), string(b1.Secret()))
|
||||
}
|
||||
if b2 := cfg.GetBackend(u2); b2 == nil {
|
||||
t.Error("didn't get backend")
|
||||
} else if !bytes.Equal(b2.Secret(), []byte(string(testBackendSecret)+"-backend2")) {
|
||||
t.Errorf("expected secret %s, got %s", string(testBackendSecret)+"-backend2", string(b2.Secret()))
|
||||
}
|
||||
|
||||
updated_config := goconf.NewConfigFile()
|
||||
updated_config.AddOption("backend", "backends", "backend1, backend2")
|
||||
updated_config.AddOption("backend", "secret", string(testBackendSecret))
|
||||
updated_config.AddOption("backend1", "url", u1.String())
|
||||
updated_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1")
|
||||
updated_config.AddOption("backend2", "url", u2.String())
|
||||
cfg.Reload(updated_config)
|
||||
|
||||
if b1 := cfg.GetBackend(u1); b1 == nil {
|
||||
t.Error("didn't get backend")
|
||||
} else if !bytes.Equal(b1.Secret(), []byte(string(testBackendSecret)+"-backend1")) {
|
||||
t.Errorf("expected secret %s, got %s", string(testBackendSecret)+"-backend1", string(b1.Secret()))
|
||||
}
|
||||
if b2 := cfg.GetBackend(u2); b2 == nil {
|
||||
t.Error("didn't get backend")
|
||||
} else if !bytes.Equal(b2.Secret(), testBackendSecret) {
|
||||
t.Errorf("expected secret %s, got %s", string(testBackendSecret), string(b2.Secret()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,13 @@
|
|||
package signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -34,8 +36,10 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
|
@ -53,7 +57,7 @@ const (
|
|||
|
||||
type BackendServer struct {
|
||||
hub *Hub
|
||||
nats NatsClient
|
||||
events AsyncEvents
|
||||
roomSessions RoomSessions
|
||||
|
||||
version string
|
||||
|
@ -64,7 +68,7 @@ type BackendServer struct {
|
|||
turnvalid time.Duration
|
||||
turnservers []string
|
||||
|
||||
statsAllowedIps map[string]bool
|
||||
statsAllowedIps atomic.Pointer[AllowedIps]
|
||||
invalidSecret []byte
|
||||
}
|
||||
|
||||
|
@ -99,21 +103,16 @@ func NewBackendServer(config *goconf.ConfigFile, hub *Hub, version string) (*Bac
|
|||
}
|
||||
|
||||
statsAllowed, _ := config.GetString("stats", "allowed_ips")
|
||||
var statsAllowedIps map[string]bool
|
||||
if statsAllowed == "" {
|
||||
log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1")
|
||||
statsAllowedIps = map[string]bool{
|
||||
"127.0.0.1": true,
|
||||
}
|
||||
statsAllowedIps, err := ParseAllowedIps(statsAllowed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !statsAllowedIps.Empty() {
|
||||
log.Printf("Only allowing access to the stats endpoint from %s", statsAllowed)
|
||||
} else {
|
||||
log.Printf("Only allowing access to the stats endpoing from %s", statsAllowed)
|
||||
statsAllowedIps = make(map[string]bool)
|
||||
for _, ip := range strings.Split(statsAllowed, ",") {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip != "" {
|
||||
statsAllowedIps[ip] = true
|
||||
}
|
||||
}
|
||||
log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1")
|
||||
statsAllowedIps = DefaultAllowedIps()
|
||||
}
|
||||
|
||||
invalidSecret := make([]byte, 32)
|
||||
|
@ -121,9 +120,9 @@ func NewBackendServer(config *goconf.ConfigFile, hub *Hub, version string) (*Bac
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &BackendServer{
|
||||
result := &BackendServer{
|
||||
hub: hub,
|
||||
nats: hub.nats,
|
||||
events: hub.events,
|
||||
roomSessions: hub.roomSessions,
|
||||
version: version,
|
||||
|
||||
|
@ -132,9 +131,27 @@ func NewBackendServer(config *goconf.ConfigFile, hub *Hub, version string) (*Bac
|
|||
turnvalid: turnvalid,
|
||||
turnservers: turnserverslist,
|
||||
|
||||
statsAllowedIps: statsAllowedIps,
|
||||
invalidSecret: invalidSecret,
|
||||
}, nil
|
||||
invalidSecret: invalidSecret,
|
||||
}
|
||||
|
||||
result.statsAllowedIps.Store(statsAllowedIps)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *BackendServer) Reload(config *goconf.ConfigFile) {
|
||||
statsAllowed, _ := config.GetString("stats", "allowed_ips")
|
||||
if statsAllowedIps, err := ParseAllowedIps(statsAllowed); err == nil {
|
||||
if !statsAllowedIps.Empty() {
|
||||
log.Printf("Only allowing access to the stats endpoint from %s", statsAllowed)
|
||||
} else {
|
||||
log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1")
|
||||
statsAllowedIps = DefaultAllowedIps()
|
||||
}
|
||||
b.statsAllowedIps.Store(statsAllowedIps)
|
||||
} else {
|
||||
log.Printf("Error parsing allowed stats ips from \"%s\": %s", statsAllowedIps, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BackendServer) Start(r *mux.Router) error {
|
||||
|
@ -278,46 +295,54 @@ func (b *BackendServer) parseRequestBody(f func(http.ResponseWriter, *http.Reque
|
|||
}
|
||||
}
|
||||
|
||||
func (b *BackendServer) sendRoomInvite(roomid string, backend *Backend, userids []string, properties *json.RawMessage) {
|
||||
msg := &ServerMessage{
|
||||
Type: "event",
|
||||
Event: &EventServerMessage{
|
||||
Target: "roomlist",
|
||||
Type: "invite",
|
||||
Invite: &RoomEventServerMessage{
|
||||
RoomId: roomid,
|
||||
Properties: properties,
|
||||
func (b *BackendServer) sendRoomInvite(roomid string, backend *Backend, userids []string, properties json.RawMessage) {
|
||||
msg := &AsyncMessage{
|
||||
Type: "message",
|
||||
Message: &ServerMessage{
|
||||
Type: "event",
|
||||
Event: &EventServerMessage{
|
||||
Target: "roomlist",
|
||||
Type: "invite",
|
||||
Invite: &RoomEventServerMessage{
|
||||
RoomId: roomid,
|
||||
Properties: properties,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, userid := range userids {
|
||||
if err := b.nats.PublishMessage(GetSubjectForUserId(userid, backend), msg); err != nil {
|
||||
if err := b.events.PublishUserMessage(userid, backend, msg); err != nil {
|
||||
log.Printf("Could not publish room invite for user %s in backend %s: %s", userid, backend.Id(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BackendServer) sendRoomDisinvite(roomid string, backend *Backend, reason string, userids []string, sessionids []string) {
|
||||
msg := &ServerMessage{
|
||||
Type: "event",
|
||||
Event: &EventServerMessage{
|
||||
Target: "roomlist",
|
||||
Type: "disinvite",
|
||||
Disinvite: &RoomDisinviteEventServerMessage{
|
||||
RoomEventServerMessage: RoomEventServerMessage{
|
||||
RoomId: roomid,
|
||||
msg := &AsyncMessage{
|
||||
Type: "message",
|
||||
Message: &ServerMessage{
|
||||
Type: "event",
|
||||
Event: &EventServerMessage{
|
||||
Target: "roomlist",
|
||||
Type: "disinvite",
|
||||
Disinvite: &RoomDisinviteEventServerMessage{
|
||||
RoomEventServerMessage: RoomEventServerMessage{
|
||||
RoomId: roomid,
|
||||
},
|
||||
Reason: reason,
|
||||
},
|
||||
Reason: reason,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, userid := range userids {
|
||||
if err := b.nats.PublishMessage(GetSubjectForUserId(userid, backend), msg); err != nil {
|
||||
if err := b.events.PublishUserMessage(userid, backend, msg); err != nil {
|
||||
log.Printf("Could not publish room disinvite for user %s in backend %s: %s", userid, backend.Id(), err)
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
for _, sessionid := range sessionids {
|
||||
if sessionid == sessionIdNotInMeeting {
|
||||
|
@ -328,10 +353,10 @@ func (b *BackendServer) sendRoomDisinvite(roomid string, backend *Backend, reaso
|
|||
wg.Add(1)
|
||||
go func(sessionid string) {
|
||||
defer wg.Done()
|
||||
if sid, err := b.lookupByRoomSessionId(sessionid, nil, timeout); err != nil {
|
||||
if sid, err := b.lookupByRoomSessionId(ctx, sessionid, nil); err != nil {
|
||||
log.Printf("Could not lookup by room session %s: %s", sessionid, err)
|
||||
} else if sid != "" {
|
||||
if err := b.nats.PublishMessage("session."+sid, msg); err != nil {
|
||||
if err := b.events.PublishSessionMessage(sid, backend, msg); err != nil {
|
||||
log.Printf("Could not publish room disinvite for session %s: %s", sid, err)
|
||||
}
|
||||
}
|
||||
|
@ -340,15 +365,18 @@ func (b *BackendServer) sendRoomDisinvite(roomid string, backend *Backend, reaso
|
|||
wg.Wait()
|
||||
}
|
||||
|
||||
func (b *BackendServer) sendRoomUpdate(roomid string, backend *Backend, notified_userids []string, all_userids []string, properties *json.RawMessage) {
|
||||
msg := &ServerMessage{
|
||||
Type: "event",
|
||||
Event: &EventServerMessage{
|
||||
Target: "roomlist",
|
||||
Type: "update",
|
||||
Update: &RoomEventServerMessage{
|
||||
RoomId: roomid,
|
||||
Properties: properties,
|
||||
func (b *BackendServer) sendRoomUpdate(roomid string, backend *Backend, notified_userids []string, all_userids []string, properties json.RawMessage) {
|
||||
msg := &AsyncMessage{
|
||||
Type: "message",
|
||||
Message: &ServerMessage{
|
||||
Type: "event",
|
||||
Event: &EventServerMessage{
|
||||
Target: "roomlist",
|
||||
Type: "update",
|
||||
Update: &RoomEventServerMessage{
|
||||
RoomId: roomid,
|
||||
Properties: properties,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -362,13 +390,13 @@ func (b *BackendServer) sendRoomUpdate(roomid string, backend *Backend, notified
|
|||
continue
|
||||
}
|
||||
|
||||
if err := b.nats.PublishMessage(GetSubjectForUserId(userid, backend), msg); err != nil {
|
||||
if err := b.events.PublishUserMessage(userid, backend, msg); err != nil {
|
||||
log.Printf("Could not publish room update for user %s in backend %s: %s", userid, backend.Id(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BackendServer) lookupByRoomSessionId(roomSessionId string, cache *ConcurrentStringStringMap, timeout time.Duration) (string, error) {
|
||||
func (b *BackendServer) lookupByRoomSessionId(ctx context.Context, roomSessionId string, cache *ConcurrentStringStringMap) (string, error) {
|
||||
if roomSessionId == sessionIdNotInMeeting {
|
||||
log.Printf("Trying to lookup empty room session id: %s", roomSessionId)
|
||||
return "", nil
|
||||
|
@ -380,7 +408,7 @@ func (b *BackendServer) lookupByRoomSessionId(roomSessionId string, cache *Concu
|
|||
}
|
||||
}
|
||||
|
||||
sid, err := b.roomSessions.GetSessionId(roomSessionId)
|
||||
sid, err := b.roomSessions.LookupSessionId(ctx, roomSessionId, "")
|
||||
if err == ErrNoSuchRoomSession {
|
||||
return "", nil
|
||||
} else if err != nil {
|
||||
|
@ -393,7 +421,7 @@ func (b *BackendServer) lookupByRoomSessionId(roomSessionId string, cache *Concu
|
|||
return sid, nil
|
||||
}
|
||||
|
||||
func (b *BackendServer) fixupUserSessions(cache *ConcurrentStringStringMap, users []map[string]interface{}, timeout time.Duration) []map[string]interface{} {
|
||||
func (b *BackendServer) fixupUserSessions(ctx context.Context, cache *ConcurrentStringStringMap, users []map[string]interface{}) []map[string]interface{} {
|
||||
if len(users) == 0 {
|
||||
return users
|
||||
}
|
||||
|
@ -421,7 +449,7 @@ func (b *BackendServer) fixupUserSessions(cache *ConcurrentStringStringMap, user
|
|||
wg.Add(1)
|
||||
go func(roomSessionId string, u map[string]interface{}) {
|
||||
defer wg.Done()
|
||||
if sessionId, err := b.lookupByRoomSessionId(roomSessionId, cache, timeout); err != nil {
|
||||
if sessionId, err := b.lookupByRoomSessionId(ctx, roomSessionId, cache); err != nil {
|
||||
log.Printf("Could not lookup by room session %s: %s", roomSessionId, err)
|
||||
delete(u, "sessionId")
|
||||
} else if sessionId != "" {
|
||||
|
@ -447,27 +475,35 @@ func (b *BackendServer) sendRoomIncall(roomid string, backend *Backend, request
|
|||
if !request.InCall.All {
|
||||
timeout := time.Second
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
var cache ConcurrentStringStringMap
|
||||
// Convert (Nextcloud) session ids to signaling session ids.
|
||||
request.InCall.Users = b.fixupUserSessions(&cache, request.InCall.Users, timeout)
|
||||
request.InCall.Users = b.fixupUserSessions(ctx, &cache, request.InCall.Users)
|
||||
// Entries in "Changed" are most likely already fetched through the "Users" list.
|
||||
request.InCall.Changed = b.fixupUserSessions(&cache, request.InCall.Changed, timeout)
|
||||
request.InCall.Changed = b.fixupUserSessions(ctx, &cache, request.InCall.Changed)
|
||||
|
||||
if len(request.InCall.Users) == 0 && len(request.InCall.Changed) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return b.nats.PublishBackendServerRoomRequest(GetSubjectForBackendRoomId(roomid, backend), request)
|
||||
message := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: request,
|
||||
}
|
||||
return b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||
}
|
||||
|
||||
func (b *BackendServer) sendRoomParticipantsUpdate(roomid string, backend *Backend, request *BackendServerRoomRequest) error {
|
||||
timeout := time.Second
|
||||
|
||||
// Convert (Nextcloud) session ids to signaling session ids.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
var cache ConcurrentStringStringMap
|
||||
request.Participants.Users = b.fixupUserSessions(&cache, request.Participants.Users, timeout)
|
||||
request.Participants.Changed = b.fixupUserSessions(&cache, request.Participants.Changed, timeout)
|
||||
request.Participants.Users = b.fixupUserSessions(ctx, &cache, request.Participants.Users)
|
||||
request.Participants.Changed = b.fixupUserSessions(ctx, &cache, request.Participants.Changed)
|
||||
|
||||
if len(request.Participants.Users) == 0 && len(request.Participants.Changed) == 0 {
|
||||
return nil
|
||||
|
@ -500,25 +536,259 @@ loop:
|
|||
|
||||
go func(sessionId string, permissions []Permission) {
|
||||
defer wg.Done()
|
||||
message := &NatsMessage{
|
||||
message := &AsyncMessage{
|
||||
Type: "permissions",
|
||||
Permissions: permissions,
|
||||
}
|
||||
if err := b.nats.Publish("session."+sessionId, message); err != nil {
|
||||
if err := b.events.PublishSessionMessage(sessionId, backend, message); err != nil {
|
||||
log.Printf("Could not send permissions update (%+v) to session %s: %s", permissions, sessionId, err)
|
||||
}
|
||||
}(sessionId, permissions)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return b.nats.PublishBackendServerRoomRequest(GetSubjectForBackendRoomId(roomid, backend), request)
|
||||
message := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: request,
|
||||
}
|
||||
return b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||
}
|
||||
|
||||
func (b *BackendServer) sendRoomMessage(roomid string, backend *Backend, request *BackendServerRoomRequest) error {
|
||||
return b.nats.PublishBackendServerRoomRequest(GetSubjectForBackendRoomId(roomid, backend), request)
|
||||
message := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: request,
|
||||
}
|
||||
return b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||
}
|
||||
|
||||
func (b *BackendServer) sendRoomSwitchTo(roomid string, backend *Backend, request *BackendServerRoomRequest) error {
|
||||
timeout := time.Second
|
||||
|
||||
// Convert (Nextcloud) session ids to signaling session ids.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
if len(request.SwitchTo.Sessions) > 0 {
|
||||
// We support both a list of sessions or a map with additional details per session.
|
||||
if request.SwitchTo.Sessions[0] == '[' {
|
||||
var sessionsList BackendRoomSwitchToSessionsList
|
||||
if err := json.Unmarshal(request.SwitchTo.Sessions, &sessionsList); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(sessionsList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var internalSessionsList BackendRoomSwitchToSessionsList
|
||||
for _, roomSessionId := range sessionsList {
|
||||
if roomSessionId == sessionIdNotInMeeting {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(roomSessionId string) {
|
||||
defer wg.Done()
|
||||
if sessionId, err := b.lookupByRoomSessionId(ctx, roomSessionId, nil); err != nil {
|
||||
log.Printf("Could not lookup by room session %s: %s", roomSessionId, err)
|
||||
} else if sessionId != "" {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
internalSessionsList = append(internalSessionsList, sessionId)
|
||||
}
|
||||
}(roomSessionId)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(internalSessionsList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
request.SwitchTo.SessionsList = internalSessionsList
|
||||
request.SwitchTo.SessionsMap = nil
|
||||
} else {
|
||||
var sessionsMap BackendRoomSwitchToSessionsMap
|
||||
if err := json.Unmarshal(request.SwitchTo.Sessions, &sessionsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(sessionsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
internalSessionsMap := make(BackendRoomSwitchToSessionsMap)
|
||||
for roomSessionId, details := range sessionsMap {
|
||||
if roomSessionId == sessionIdNotInMeeting {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(roomSessionId string, details json.RawMessage) {
|
||||
defer wg.Done()
|
||||
if sessionId, err := b.lookupByRoomSessionId(ctx, roomSessionId, nil); err != nil {
|
||||
log.Printf("Could not lookup by room session %s: %s", roomSessionId, err)
|
||||
} else if sessionId != "" {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
internalSessionsMap[sessionId] = details
|
||||
}
|
||||
}(roomSessionId, details)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(internalSessionsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
request.SwitchTo.SessionsList = nil
|
||||
request.SwitchTo.SessionsMap = internalSessionsMap
|
||||
}
|
||||
}
|
||||
request.SwitchTo.Sessions = nil
|
||||
|
||||
message := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: request,
|
||||
}
|
||||
return b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||
}
|
||||
|
||||
type BackendResponseWithStatus interface {
|
||||
Status() int
|
||||
}
|
||||
|
||||
type DialoutErrorResponse struct {
|
||||
BackendServerRoomResponse
|
||||
|
||||
status int
|
||||
}
|
||||
|
||||
func (r *DialoutErrorResponse) Status() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
func returnDialoutError(status int, err *Error) (any, error) {
|
||||
response := &DialoutErrorResponse{
|
||||
BackendServerRoomResponse: BackendServerRoomResponse{
|
||||
Type: "dialout",
|
||||
Dialout: &BackendRoomDialoutResponse{
|
||||
Error: err,
|
||||
},
|
||||
},
|
||||
|
||||
status: status,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
var checkNumeric = regexp.MustCompile(`^[0-9]+$`)
|
||||
|
||||
func isNumeric(s string) bool {
|
||||
return checkNumeric.MatchString(s)
|
||||
}
|
||||
|
||||
func (b *BackendServer) startDialout(roomid string, backend *Backend, backendUrl string, request *BackendServerRoomRequest) (any, error) {
|
||||
if err := request.Dialout.ValidateNumber(); err != nil {
|
||||
return returnDialoutError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
if !isNumeric(roomid) {
|
||||
return returnDialoutError(http.StatusBadRequest, NewError("invalid_roomid", "The room id must be numeric."))
|
||||
}
|
||||
|
||||
session := b.hub.GetDialoutSession(roomid, backend)
|
||||
if session == nil {
|
||||
return returnDialoutError(http.StatusNotFound, NewError("no_client_available", "No available client found to trigger dialout."))
|
||||
}
|
||||
|
||||
url := backend.Url()
|
||||
if url == "" {
|
||||
// Old-style compat backend, use client-provided URL.
|
||||
url = backendUrl
|
||||
if url != "" && url[len(url)-1] != '/' {
|
||||
url += "/"
|
||||
}
|
||||
}
|
||||
id := newRandomString(32)
|
||||
msg := &ServerMessage{
|
||||
Id: id,
|
||||
Type: "internal",
|
||||
Internal: &InternalServerMessage{
|
||||
Type: "dialout",
|
||||
Dialout: &InternalServerDialoutRequest{
|
||||
RoomId: roomid,
|
||||
Backend: url,
|
||||
Request: request.Dialout,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var response atomic.Pointer[DialoutInternalClientMessage]
|
||||
|
||||
session.HandleResponse(id, func(message *ClientMessage) bool {
|
||||
response.Store(message.Internal.Dialout)
|
||||
cancel()
|
||||
// Don't send error to other sessions in the room.
|
||||
return message.Internal.Dialout.Error != nil
|
||||
})
|
||||
defer session.ClearResponseHandler(id)
|
||||
|
||||
if !session.SendMessage(msg) {
|
||||
return returnDialoutError(http.StatusBadGateway, NewError("error_notify", "Could not notify about new dialout."))
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return returnDialoutError(http.StatusGatewayTimeout, NewError("timeout", "Timeout while waiting for dialout to start."))
|
||||
}
|
||||
|
||||
dialout := response.Load()
|
||||
if dialout == nil {
|
||||
return returnDialoutError(http.StatusBadGateway, NewError("error_notify", "No dialout response received."))
|
||||
}
|
||||
|
||||
switch dialout.Type {
|
||||
case "error":
|
||||
return returnDialoutError(http.StatusBadGateway, dialout.Error)
|
||||
case "status":
|
||||
if dialout.Status.Status != DialoutStatusAccepted {
|
||||
log.Printf("Received unsupported dialout status when triggering dialout: %+v", dialout)
|
||||
return returnDialoutError(http.StatusBadGateway, NewError("unsupported_status", "Unsupported dialout status received."))
|
||||
}
|
||||
|
||||
return &BackendServerRoomResponse{
|
||||
Type: "dialout",
|
||||
Dialout: &BackendRoomDialoutResponse{
|
||||
CallId: dialout.Status.CallId,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
log.Printf("Received unsupported dialout type when triggering dialout: %+v", dialout)
|
||||
return returnDialoutError(http.StatusBadGateway, NewError("unsupported_type", "Unsupported dialout type received."))
|
||||
}
|
||||
|
||||
func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body []byte) {
|
||||
throttle, err := b.hub.throttler.CheckBruteforce(r.Context(), b.hub.getRealUserIP(r), "BackendRoomAuth")
|
||||
if err == ErrBruteforceDetected {
|
||||
http.Error(w, "Too many requests", http.StatusTooManyRequests)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Printf("Error checking for bruteforce: %s", err)
|
||||
http.Error(w, "Could not check for bruteforce", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
v := mux.Vars(r)
|
||||
roomid := v["roomid"]
|
||||
|
||||
|
@ -531,6 +801,7 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
|||
|
||||
if backend == nil {
|
||||
// Unknown backend URL passed, return immediately.
|
||||
throttle(r.Context())
|
||||
http.Error(w, "Authentication check failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
@ -552,12 +823,14 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
|||
}
|
||||
|
||||
if backend == nil {
|
||||
throttle(r.Context())
|
||||
http.Error(w, "Authentication check failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !ValidateBackendChecksum(r, body, backend.Secret()) {
|
||||
throttle(r.Context())
|
||||
http.Error(w, "Authentication check failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
@ -571,7 +844,7 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
|||
|
||||
request.ReceivedTime = time.Now().UnixNano()
|
||||
|
||||
var err error
|
||||
var response any
|
||||
switch request.Type {
|
||||
case "invite":
|
||||
b.sendRoomInvite(roomid, backend, request.Invite.UserIds, request.Invite.Properties)
|
||||
|
@ -580,10 +853,18 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
|||
b.sendRoomDisinvite(roomid, backend, DisinviteReasonDisinvited, request.Disinvite.UserIds, request.Disinvite.SessionIds)
|
||||
b.sendRoomUpdate(roomid, backend, request.Disinvite.UserIds, request.Disinvite.AllUserIds, request.Disinvite.Properties)
|
||||
case "update":
|
||||
err = b.nats.PublishBackendServerRoomRequest(GetSubjectForBackendRoomId(roomid, backend), &request)
|
||||
message := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: &request,
|
||||
}
|
||||
err = b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||
b.sendRoomUpdate(roomid, backend, nil, request.Update.UserIds, request.Update.Properties)
|
||||
case "delete":
|
||||
err = b.nats.PublishBackendServerRoomRequest(GetSubjectForBackendRoomId(roomid, backend), &request)
|
||||
message := &AsyncMessage{
|
||||
Type: "room",
|
||||
Room: &request,
|
||||
}
|
||||
err = b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||
b.sendRoomDisinvite(roomid, backend, DisinviteReasonDeleted, request.Delete.UserIds, nil)
|
||||
case "incall":
|
||||
err = b.sendRoomIncall(roomid, backend, &request)
|
||||
|
@ -591,6 +872,10 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
|||
err = b.sendRoomParticipantsUpdate(roomid, backend, &request)
|
||||
case "message":
|
||||
err = b.sendRoomMessage(roomid, backend, &request)
|
||||
case "switchto":
|
||||
err = b.sendRoomSwitchTo(roomid, backend, &request)
|
||||
case "dialout":
|
||||
response, err = b.startDialout(roomid, backend, backendUrl, &request)
|
||||
default:
|
||||
http.Error(w, "Unsupported request type: "+request.Type, http.StatusBadRequest)
|
||||
return
|
||||
|
@ -602,22 +887,43 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
|||
return
|
||||
}
|
||||
|
||||
var responseData []byte
|
||||
responseStatus := http.StatusOK
|
||||
if response == nil {
|
||||
// TODO(jojo): Return better response struct.
|
||||
responseData = []byte("{}")
|
||||
} else {
|
||||
if s, ok := response.(BackendResponseWithStatus); ok {
|
||||
responseStatus = s.Status()
|
||||
}
|
||||
responseData, err = json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Printf("Could not serialize backend response %+v: %s", response, err)
|
||||
responseStatus = http.StatusInternalServerError
|
||||
responseData = []byte("{\"error\":\"could_not_serialize\"}")
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// TODO(jojo): Return better response struct.
|
||||
w.Write([]byte("{}")) // nolint
|
||||
w.WriteHeader(responseStatus)
|
||||
w.Write(responseData) // nolint
|
||||
}
|
||||
|
||||
func (b *BackendServer) allowStatsAccess(r *http.Request) bool {
|
||||
addr := b.hub.getRealUserIP(r)
|
||||
ip := net.ParseIP(addr)
|
||||
if len(ip) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
allowed := b.statsAllowedIps.Load()
|
||||
return allowed != nil && allowed.Allowed(ip)
|
||||
}
|
||||
|
||||
func (b *BackendServer) validateStatsRequest(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
addr := getRealUserIP(r)
|
||||
if strings.Contains(addr, ":") {
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
}
|
||||
if !b.statsAllowedIps[addr] {
|
||||
if !b.allowStatsAccess(r) {
|
||||
http.Error(w, "Authentication check failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
282
backend_storage_etcd.go
Normal file
282
backend_storage_etcd.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
/**
|
||||
* 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"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
type backendStorageEtcd struct {
|
||||
backendStorageCommon
|
||||
|
||||
etcdClient *EtcdClient
|
||||
keyPrefix string
|
||||
keyInfos map[string]*BackendInformationEtcd
|
||||
|
||||
initializedCtx context.Context
|
||||
initializedFunc context.CancelFunc
|
||||
wakeupChanForTesting chan struct{}
|
||||
|
||||
closeCtx context.Context
|
||||
closeFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewBackendStorageEtcd(config *goconf.ConfigFile, etcdClient *EtcdClient) (BackendStorage, error) {
|
||||
if etcdClient == nil || !etcdClient.IsConfigured() {
|
||||
return nil, fmt.Errorf("no etcd endpoints configured")
|
||||
}
|
||||
|
||||
keyPrefix, _ := config.GetString("backend", "backendprefix")
|
||||
if keyPrefix == "" {
|
||||
return nil, fmt.Errorf("no backend prefix configured")
|
||||
}
|
||||
|
||||
initializedCtx, initializedFunc := context.WithCancel(context.Background())
|
||||
closeCtx, closeFunc := context.WithCancel(context.Background())
|
||||
result := &backendStorageEtcd{
|
||||
backendStorageCommon: backendStorageCommon{
|
||||
backends: make(map[string][]*Backend),
|
||||
},
|
||||
etcdClient: etcdClient,
|
||||
keyPrefix: keyPrefix,
|
||||
keyInfos: make(map[string]*BackendInformationEtcd),
|
||||
|
||||
initializedCtx: initializedCtx,
|
||||
initializedFunc: initializedFunc,
|
||||
closeCtx: closeCtx,
|
||||
closeFunc: closeFunc,
|
||||
}
|
||||
|
||||
etcdClient.AddListener(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) WaitForInitialized(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-s.initializedCtx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) wakeupForTesting() {
|
||||
if s.wakeupChanForTesting == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case s.wakeupChanForTesting <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) EtcdClientCreated(client *EtcdClient) {
|
||||
go func() {
|
||||
if err := client.WaitForConnection(s.closeCtx); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
panic(err)
|
||||
}
|
||||
|
||||
backoff, err := NewExponentialBackoff(initialWaitDelay, maxWaitDelay)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for s.closeCtx.Err() == nil {
|
||||
response, err := s.getBackends(s.closeCtx, client, s.keyPrefix)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Printf("Timeout getting initial list of backends, retry in %s", backoff.NextWait())
|
||||
} else {
|
||||
log.Printf("Could not get initial list of backends, retry in %s: %s", backoff.NextWait(), err)
|
||||
}
|
||||
|
||||
backoff.Wait(s.closeCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ev := range response.Kvs {
|
||||
s.EtcdKeyUpdated(client, string(ev.Key), ev.Value, nil)
|
||||
}
|
||||
s.initializedFunc()
|
||||
|
||||
nextRevision := response.Header.Revision + 1
|
||||
prevRevision := nextRevision
|
||||
backoff.Reset()
|
||||
for s.closeCtx.Err() == nil {
|
||||
var err error
|
||||
if nextRevision, err = client.Watch(s.closeCtx, s.keyPrefix, nextRevision, s, clientv3.WithPrefix()); err != nil {
|
||||
log.Printf("Error processing watch for %s (%s), retry in %s", s.keyPrefix, err, backoff.NextWait())
|
||||
backoff.Wait(s.closeCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
if nextRevision != prevRevision {
|
||||
backoff.Reset()
|
||||
prevRevision = nextRevision
|
||||
} else {
|
||||
log.Printf("Processing watch for %s interrupted, retry in %s", s.keyPrefix, backoff.NextWait())
|
||||
backoff.Wait(s.closeCtx)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) EtcdWatchCreated(client *EtcdClient, key string) {
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) getBackends(ctx context.Context, client *EtcdClient, keyPrefix string) (*clientv3.GetResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
return client.Get(ctx, keyPrefix, clientv3.WithPrefix())
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) EtcdKeyUpdated(client *EtcdClient, key string, data []byte, prevValue []byte) {
|
||||
var info BackendInformationEtcd
|
||||
if err := json.Unmarshal(data, &info); err != nil {
|
||||
log.Printf("Could not decode backend information %s: %s", string(data), err)
|
||||
return
|
||||
}
|
||||
if err := info.CheckValid(); err != nil {
|
||||
log.Printf("Received invalid backend information %s: %s", string(data), err)
|
||||
return
|
||||
}
|
||||
|
||||
backend := &Backend{
|
||||
id: key,
|
||||
url: info.Url,
|
||||
parsedUrl: info.parsedUrl,
|
||||
secret: []byte(info.Secret),
|
||||
|
||||
allowHttp: info.parsedUrl.Scheme == "http",
|
||||
|
||||
maxStreamBitrate: info.MaxStreamBitrate,
|
||||
maxScreenBitrate: info.MaxScreenBitrate,
|
||||
sessionLimit: info.SessionLimit,
|
||||
}
|
||||
|
||||
host := info.parsedUrl.Host
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.keyInfos[key] = &info
|
||||
entries, found := s.backends[host]
|
||||
if !found {
|
||||
// Simple case, first backend for this host
|
||||
log.Printf("Added backend %s (from %s)", info.Url, key)
|
||||
s.backends[host] = []*Backend{backend}
|
||||
statsBackendsCurrent.Inc()
|
||||
s.wakeupForTesting()
|
||||
return
|
||||
}
|
||||
|
||||
// Was the backend changed?
|
||||
replaced := false
|
||||
for idx, entry := range entries {
|
||||
if entry.id == key {
|
||||
log.Printf("Updated backend %s (from %s)", info.Url, key)
|
||||
entries[idx] = backend
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced {
|
||||
// New backend, add to list.
|
||||
log.Printf("Added backend %s (from %s)", info.Url, key)
|
||||
s.backends[host] = append(entries, backend)
|
||||
statsBackendsCurrent.Inc()
|
||||
}
|
||||
s.wakeupForTesting()
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
info, found := s.keyInfos[key]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
delete(s.keyInfos, key)
|
||||
host := info.parsedUrl.Host
|
||||
entries, found := s.backends[host]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Removing backend %s (from %s)", info.Url, key)
|
||||
newEntries := make([]*Backend, 0, len(entries)-1)
|
||||
for _, entry := range entries {
|
||||
if entry.id == key {
|
||||
statsBackendsCurrent.Dec()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntries = append(newEntries, entry)
|
||||
}
|
||||
if len(newEntries) > 0 {
|
||||
s.backends[host] = newEntries
|
||||
} else {
|
||||
delete(s.backends, host)
|
||||
}
|
||||
s.wakeupForTesting()
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) Close() {
|
||||
s.etcdClient.RemoveListener(s)
|
||||
s.closeFunc()
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) Reload(config *goconf.ConfigFile) {
|
||||
// Backend updates are processed through etcd.
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) GetCompatBackend() *Backend {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *backendStorageEtcd) GetBackend(u *url.URL) *Backend {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.getBackendLocked(u)
|
||||
}
|
77
backend_storage_etcd_test.go
Normal file
77
backend_storage_etcd_test.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"go.etcd.io/etcd/server/v3/embed"
|
||||
)
|
||||
|
||||
func (s *backendStorageEtcd) getWakeupChannelForTesting() <-chan struct{} {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.wakeupChanForTesting != nil {
|
||||
return s.wakeupChanForTesting
|
||||
}
|
||||
|
||||
ch := make(chan struct{}, 1)
|
||||
s.wakeupChanForTesting = ch
|
||||
return ch
|
||||
}
|
||||
|
||||
type testListener struct {
|
||||
etcd *embed.Etcd
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (tl *testListener) EtcdClientCreated(client *EtcdClient) {
|
||||
tl.etcd.Server.Stop()
|
||||
close(tl.closed)
|
||||
}
|
||||
|
||||
func Test_BackendStorageEtcdNoLeak(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
ensureNoGoroutinesLeak(t, func(t *testing.T) {
|
||||
etcd, client := NewEtcdClientForTest(t)
|
||||
tl := &testListener{
|
||||
etcd: etcd,
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
client.AddListener(tl)
|
||||
defer client.RemoveListener(tl)
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("backend", "backendtype", "etcd")
|
||||
config.AddOption("backend", "backendprefix", "/backends")
|
||||
|
||||
cfg, err := NewBackendConfiguration(config, client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
<-tl.closed
|
||||
cfg.Close()
|
||||
})
|
||||
}
|
314
backend_storage_static.go
Normal file
314
backend_storage_static.go
Normal file
|
@ -0,0 +1,314 @@
|
|||
/**
|
||||
* 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
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:]...)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
76
backoff.go
Normal file
76
backoff.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 signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Backoff interface {
|
||||
Reset()
|
||||
NextWait() time.Duration
|
||||
Wait(context.Context)
|
||||
}
|
||||
|
||||
type exponentialBackoff struct {
|
||||
initial time.Duration
|
||||
maxWait time.Duration
|
||||
nextWait time.Duration
|
||||
}
|
||||
|
||||
func NewExponentialBackoff(initial time.Duration, maxWait time.Duration) (Backoff, error) {
|
||||
if initial <= 0 {
|
||||
return nil, fmt.Errorf("initial must be larger than 0")
|
||||
}
|
||||
if maxWait < initial {
|
||||
return nil, fmt.Errorf("maxWait must be larger or equal to initial")
|
||||
}
|
||||
|
||||
return &exponentialBackoff{
|
||||
initial: initial,
|
||||
maxWait: maxWait,
|
||||
|
||||
nextWait: initial,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *exponentialBackoff) Reset() {
|
||||
b.nextWait = b.initial
|
||||
}
|
||||
|
||||
func (b *exponentialBackoff) NextWait() time.Duration {
|
||||
return b.nextWait
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
<-waiter.Done()
|
||||
}
|
65
backoff_test.go
Normal file
65
backoff_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* 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"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBackoff_Exponential(t *testing.T) {
|
||||
t.Parallel()
|
||||
backoff, err := NewExponentialBackoff(100*time.Millisecond, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
waitTimes := []time.Duration{
|
||||
100 * time.Millisecond,
|
||||
200 * time.Millisecond,
|
||||
400 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
}
|
||||
|
||||
for _, wait := range waitTimes {
|
||||
if backoff.NextWait() != wait {
|
||||
t.Errorf("Wait time should be %s, got %s", wait, backoff.NextWait())
|
||||
}
|
||||
|
||||
a := time.Now()
|
||||
backoff.Wait(context.Background())
|
||||
b := time.Now()
|
||||
if b.Sub(a) < wait {
|
||||
t.Errorf("Should have waited %s, got %s", wait, b.Sub(a))
|
||||
}
|
||||
}
|
||||
|
||||
backoff.Reset()
|
||||
a := time.Now()
|
||||
backoff.Wait(context.Background())
|
||||
b := time.Now()
|
||||
if b.Sub(a) < 100*time.Millisecond {
|
||||
t.Errorf("Should have waited %s, got %s", 100*time.Millisecond, b.Sub(a))
|
||||
}
|
||||
}
|
146
capabilities.go
146
capabilities.go
|
@ -43,6 +43,9 @@ const (
|
|||
|
||||
// Cache received capabilities for one hour.
|
||||
CapabilitiesCacheDuration = time.Hour
|
||||
|
||||
// Don't invalidate more than once per minute.
|
||||
maxInvalidateInterval = time.Minute
|
||||
)
|
||||
|
||||
type capabilitiesEntry struct {
|
||||
|
@ -53,16 +56,23 @@ type capabilitiesEntry struct {
|
|||
type Capabilities struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
version string
|
||||
pool *HttpClientPool
|
||||
entries map[string]*capabilitiesEntry
|
||||
// Can be overwritten by tests.
|
||||
getNow func() time.Time
|
||||
|
||||
version string
|
||||
pool *HttpClientPool
|
||||
entries map[string]*capabilitiesEntry
|
||||
nextInvalidate map[string]time.Time
|
||||
}
|
||||
|
||||
func NewCapabilities(version string, pool *HttpClientPool) (*Capabilities, error) {
|
||||
result := &Capabilities{
|
||||
version: version,
|
||||
pool: pool,
|
||||
entries: make(map[string]*capabilitiesEntry),
|
||||
getNow: time.Now,
|
||||
|
||||
version: version,
|
||||
pool: pool,
|
||||
entries: make(map[string]*capabilitiesEntry),
|
||||
nextInvalidate: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
@ -78,15 +88,15 @@ type CapabilitiesVersion struct {
|
|||
}
|
||||
|
||||
type CapabilitiesResponse struct {
|
||||
Version CapabilitiesVersion `json:"version"`
|
||||
Capabilities map[string]map[string]interface{} `json:"capabilities"`
|
||||
Version CapabilitiesVersion `json:"version"`
|
||||
Capabilities map[string]json.RawMessage `json:"capabilities"`
|
||||
}
|
||||
|
||||
func (c *Capabilities) getCapabilities(key string) (map[string]interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
now := c.getNow()
|
||||
if entry, found := c.entries[key]; found && entry.nextUpdate.After(now) {
|
||||
return entry.capabilities, true
|
||||
}
|
||||
|
@ -95,22 +105,40 @@ func (c *Capabilities) getCapabilities(key string) (map[string]interface{}, bool
|
|||
}
|
||||
|
||||
func (c *Capabilities) setCapabilities(key string, capabilities map[string]interface{}) {
|
||||
now := time.Now()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := c.getNow()
|
||||
entry := &capabilitiesEntry{
|
||||
nextUpdate: now.Add(CapabilitiesCacheDuration),
|
||||
capabilities: capabilities,
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.entries[key] = entry
|
||||
}
|
||||
|
||||
func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (map[string]interface{}, error) {
|
||||
key := u.String()
|
||||
func (c *Capabilities) invalidateCapabilities(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := c.getNow()
|
||||
if entry, found := c.nextInvalidate[key]; found && entry.After(now) {
|
||||
return
|
||||
}
|
||||
|
||||
delete(c.entries, key)
|
||||
c.nextInvalidate[key] = now.Add(maxInvalidateInterval)
|
||||
}
|
||||
|
||||
func (c *Capabilities) getKeyForUrl(u *url.URL) string {
|
||||
key := u.String()
|
||||
return key
|
||||
}
|
||||
|
||||
func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (map[string]interface{}, bool, error) {
|
||||
key := c.getKeyForUrl(u)
|
||||
if caps, found := c.getCapabilities(key); found {
|
||||
return caps, nil
|
||||
return caps, true, nil
|
||||
}
|
||||
|
||||
capUrl := *u
|
||||
|
@ -128,14 +156,14 @@ func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (map[st
|
|||
client, pool, err := c.pool.Get(ctx, &capUrl)
|
||||
if err != nil {
|
||||
log.Printf("Could not get client for host %s: %s", capUrl.Host, err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
defer pool.Put(client)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", capUrl.String(), nil)
|
||||
if err != nil {
|
||||
log.Printf("Could not create request to %s: %s", &capUrl, err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("OCS-APIRequest", "true")
|
||||
|
@ -143,50 +171,56 @@ func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (map[st
|
|||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, 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)", capUrl.String(), ct, resp.Status)
|
||||
return nil, ErrUnsupportedContentType
|
||||
return nil, false, ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("Could not read response body from %s: %s", capUrl.String(), err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var ocs OcsResponse
|
||||
if err := json.Unmarshal(body, &ocs); err != nil {
|
||||
log.Printf("Could not decode OCS response %s from %s: %s", string(body), capUrl.String(), err)
|
||||
return nil, err
|
||||
} else if ocs.Ocs == nil || ocs.Ocs.Data == nil {
|
||||
return nil, false, err
|
||||
} else if ocs.Ocs == nil || len(ocs.Ocs.Data) == 0 {
|
||||
log.Printf("Incomplete OCS response %s from %s", string(body), u)
|
||||
return nil, fmt.Errorf("incomplete OCS response")
|
||||
return nil, false, fmt.Errorf("incomplete OCS response")
|
||||
}
|
||||
|
||||
var response CapabilitiesResponse
|
||||
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), capUrl.String(), err)
|
||||
return nil, err
|
||||
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), capUrl.String(), err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
capa, found := response.Capabilities[AppNameSpreed]
|
||||
if !found {
|
||||
capaObj, found := response.Capabilities[AppNameSpreed]
|
||||
if !found || len(capaObj) == 0 {
|
||||
log.Printf("No capabilities received for app spreed from %s: %+v", capUrl.String(), response)
|
||||
return nil, nil
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
var capa map[string]interface{}
|
||||
if err := json.Unmarshal(capaObj, &capa); err != nil {
|
||||
log.Printf("Unsupported capabilities received for app spreed from %s: %+v", capUrl.String(), response)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
log.Printf("Received capabilities %+v from %s", capa, capUrl.String())
|
||||
c.setCapabilities(key, capa)
|
||||
return capa, nil
|
||||
return capa, false, nil
|
||||
}
|
||||
|
||||
func (c *Capabilities) HasCapabilityFeature(ctx context.Context, u *url.URL, feature string) bool {
|
||||
caps, err := c.loadCapabilities(ctx, u)
|
||||
caps, _, err := c.loadCapabilities(ctx, u)
|
||||
if err != nil {
|
||||
log.Printf("Could not get capabilities for %s: %s", u, err)
|
||||
return false
|
||||
|
@ -211,80 +245,86 @@ func (c *Capabilities) HasCapabilityFeature(ctx context.Context, u *url.URL, fea
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *Capabilities) getConfigGroup(ctx context.Context, u *url.URL, group string) (map[string]interface{}, bool) {
|
||||
caps, err := c.loadCapabilities(ctx, u)
|
||||
func (c *Capabilities) getConfigGroup(ctx context.Context, u *url.URL, group string) (map[string]interface{}, bool, bool) {
|
||||
caps, cached, err := c.loadCapabilities(ctx, u)
|
||||
if err != nil {
|
||||
log.Printf("Could not get capabilities for %s: %s", u, err)
|
||||
return nil, false
|
||||
return nil, cached, false
|
||||
}
|
||||
|
||||
configInterface := caps["config"]
|
||||
if configInterface == nil {
|
||||
return nil, false
|
||||
return nil, cached, false
|
||||
}
|
||||
|
||||
config, ok := configInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Printf("Invalid config mapping received from %s: %+v", u, configInterface)
|
||||
return nil, false
|
||||
return nil, cached, false
|
||||
}
|
||||
|
||||
groupInterface := config[group]
|
||||
if groupInterface == nil {
|
||||
return nil, false
|
||||
return nil, cached, false
|
||||
}
|
||||
|
||||
groupConfig, ok := groupInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Printf("Invalid group mapping \"%s\" received from %s: %+v", group, u, groupInterface)
|
||||
return nil, false
|
||||
return nil, cached, false
|
||||
}
|
||||
|
||||
return groupConfig, true
|
||||
return groupConfig, cached, true
|
||||
}
|
||||
|
||||
func (c *Capabilities) GetIntegerConfig(ctx context.Context, u *url.URL, group, key string) (int, bool) {
|
||||
groupConfig, found := c.getConfigGroup(ctx, u, group)
|
||||
func (c *Capabilities) GetIntegerConfig(ctx context.Context, u *url.URL, group, key string) (int, bool, bool) {
|
||||
groupConfig, cached, found := c.getConfigGroup(ctx, u, group)
|
||||
if !found {
|
||||
return 0, false
|
||||
return 0, cached, false
|
||||
}
|
||||
|
||||
value, found := groupConfig[key]
|
||||
if !found {
|
||||
return 0, false
|
||||
return 0, cached, false
|
||||
}
|
||||
|
||||
switch value := value.(type) {
|
||||
case int:
|
||||
return value, true
|
||||
return value, cached, true
|
||||
case float32:
|
||||
return int(value), true
|
||||
return int(value), cached, true
|
||||
case float64:
|
||||
return int(value), true
|
||||
return int(value), cached, true
|
||||
default:
|
||||
log.Printf("Invalid config value for \"%s\" received from %s: %+v", key, u, value)
|
||||
}
|
||||
|
||||
return 0, false
|
||||
return 0, cached, false
|
||||
}
|
||||
|
||||
func (c *Capabilities) GetStringConfig(ctx context.Context, u *url.URL, group, key string) (string, bool) {
|
||||
groupConfig, found := c.getConfigGroup(ctx, u, group)
|
||||
func (c *Capabilities) GetStringConfig(ctx context.Context, u *url.URL, group, key string) (string, bool, bool) {
|
||||
groupConfig, cached, found := c.getConfigGroup(ctx, u, group)
|
||||
if !found {
|
||||
return "", false
|
||||
return "", cached, false
|
||||
}
|
||||
|
||||
value, found := groupConfig[key]
|
||||
if !found {
|
||||
return "", false
|
||||
return "", cached, false
|
||||
}
|
||||
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
return value, true
|
||||
return value, cached, true
|
||||
default:
|
||||
log.Printf("Invalid config value for \"%s\" received from %s: %+v", key, u, value)
|
||||
}
|
||||
|
||||
return "", false
|
||||
return "", cached, false
|
||||
}
|
||||
|
||||
func (c *Capabilities) InvalidateCapabilities(u *url.URL) {
|
||||
key := c.getKeyForUrl(u)
|
||||
|
||||
c.invalidateCapabilities(key)
|
||||
}
|
||||
|
|
|
@ -28,12 +28,14 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func NewCapabilitiesForTest(t *testing.T) (*url.URL, *Capabilities) {
|
||||
func NewCapabilitiesForTestWithCallback(t *testing.T, callback func(*CapabilitiesResponse)) (*url.URL, *Capabilities) {
|
||||
pool, err := NewHttpClientPool(1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -69,18 +71,25 @@ func NewCapabilitiesForTest(t *testing.T) (*url.URL, *Capabilities) {
|
|||
config := map[string]interface{}{
|
||||
"signaling": signaling,
|
||||
}
|
||||
spreedCapa, _ := json.Marshal(map[string]interface{}{
|
||||
"features": features,
|
||||
"config": config,
|
||||
})
|
||||
emptyArray := []byte("[]")
|
||||
response := &CapabilitiesResponse{
|
||||
Version: CapabilitiesVersion{
|
||||
Major: 20,
|
||||
},
|
||||
Capabilities: map[string]map[string]interface{}{
|
||||
"spreed": {
|
||||
"features": features,
|
||||
"config": config,
|
||||
},
|
||||
Capabilities: map[string]json.RawMessage{
|
||||
"anotherApp": emptyArray,
|
||||
"spreed": spreedCapa,
|
||||
},
|
||||
}
|
||||
|
||||
if callback != nil {
|
||||
callback(response)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Errorf("Could not marshal %+v: %s", response, err)
|
||||
|
@ -93,7 +102,7 @@ func NewCapabilitiesForTest(t *testing.T) (*url.URL, *Capabilities) {
|
|||
StatusCode: http.StatusOK,
|
||||
Message: http.StatusText(http.StatusOK),
|
||||
},
|
||||
Data: (*json.RawMessage)(&data),
|
||||
Data: data,
|
||||
}
|
||||
if data, err = json.Marshal(ocs); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -107,7 +116,29 @@ func NewCapabilitiesForTest(t *testing.T) (*url.URL, *Capabilities) {
|
|||
return u, capabilities
|
||||
}
|
||||
|
||||
func NewCapabilitiesForTest(t *testing.T) (*url.URL, *Capabilities) {
|
||||
return NewCapabilitiesForTestWithCallback(t, nil)
|
||||
}
|
||||
|
||||
func SetCapabilitiesGetNow(t *testing.T, capabilities *Capabilities, f func() time.Time) {
|
||||
capabilities.mu.Lock()
|
||||
defer capabilities.mu.Unlock()
|
||||
|
||||
old := capabilities.getNow
|
||||
|
||||
t.Cleanup(func() {
|
||||
capabilities.mu.Lock()
|
||||
defer capabilities.mu.Unlock()
|
||||
|
||||
capabilities.getNow = old
|
||||
})
|
||||
|
||||
capabilities.getNow = f
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
url, capabilities := NewCapabilitiesForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
|
@ -121,34 +152,124 @@ func TestCapabilities(t *testing.T) {
|
|||
}
|
||||
|
||||
expectedString := "bar"
|
||||
if value, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); !found {
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); !found {
|
||||
t.Error("could not find value for \"foo\"")
|
||||
} else if value != expectedString {
|
||||
t.Errorf("expected value %s, got %s", expectedString, value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
if value, found := capabilities.GetStringConfig(ctx, url, "signaling", "baz"); found {
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "baz"); found {
|
||||
t.Errorf("should not have found value for \"baz\", got %s", value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
if value, found := capabilities.GetStringConfig(ctx, url, "signaling", "invalid"); found {
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "invalid"); found {
|
||||
t.Errorf("should not have found value for \"invalid\", got %s", value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
if value, found := capabilities.GetStringConfig(ctx, url, "invalid", "foo"); found {
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "invalid", "foo"); found {
|
||||
t.Errorf("should not have found value for \"baz\", got %s", value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
|
||||
expectedInt := 42
|
||||
if value, found := capabilities.GetIntegerConfig(ctx, url, "signaling", "baz"); !found {
|
||||
if value, cached, found := capabilities.GetIntegerConfig(ctx, url, "signaling", "baz"); !found {
|
||||
t.Error("could not find value for \"baz\"")
|
||||
} else if value != expectedInt {
|
||||
t.Errorf("expected value %d, got %d", expectedInt, value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
if value, found := capabilities.GetIntegerConfig(ctx, url, "signaling", "foo"); found {
|
||||
if value, cached, found := capabilities.GetIntegerConfig(ctx, url, "signaling", "foo"); found {
|
||||
t.Errorf("should not have found value for \"foo\", got %d", value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
if value, found := capabilities.GetIntegerConfig(ctx, url, "signaling", "invalid"); found {
|
||||
if value, cached, found := capabilities.GetIntegerConfig(ctx, url, "signaling", "invalid"); found {
|
||||
t.Errorf("should not have found value for \"invalid\", got %d", value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
if value, found := capabilities.GetIntegerConfig(ctx, url, "invalid", "baz"); found {
|
||||
if value, cached, found := capabilities.GetIntegerConfig(ctx, url, "invalid", "baz"); found {
|
||||
t.Errorf("should not have found value for \"baz\", got %d", value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateCapabilities(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
var called atomic.Uint32
|
||||
url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse) {
|
||||
called.Add(1)
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
expectedString := "bar"
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); !found {
|
||||
t.Error("could not find value for \"foo\"")
|
||||
} else if value != expectedString {
|
||||
t.Errorf("expected value %s, got %s", expectedString, value)
|
||||
} else if cached {
|
||||
t.Errorf("expected direct response")
|
||||
}
|
||||
|
||||
if value := called.Load(); value != 1 {
|
||||
t.Errorf("expected called %d, got %d", 1, value)
|
||||
}
|
||||
|
||||
// Invalidating will cause the capabilities to be reloaded.
|
||||
capabilities.InvalidateCapabilities(url)
|
||||
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); !found {
|
||||
t.Error("could not find value for \"foo\"")
|
||||
} else if value != expectedString {
|
||||
t.Errorf("expected value %s, got %s", expectedString, value)
|
||||
} else if cached {
|
||||
t.Errorf("expected direct response")
|
||||
}
|
||||
|
||||
if value := called.Load(); value != 2 {
|
||||
t.Errorf("expected called %d, got %d", 2, value)
|
||||
}
|
||||
|
||||
// Invalidating is throttled to about once per minute.
|
||||
capabilities.InvalidateCapabilities(url)
|
||||
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); !found {
|
||||
t.Error("could not find value for \"foo\"")
|
||||
} else if value != expectedString {
|
||||
t.Errorf("expected value %s, got %s", expectedString, value)
|
||||
} else if !cached {
|
||||
t.Errorf("expected cached response")
|
||||
}
|
||||
|
||||
if value := called.Load(); value != 2 {
|
||||
t.Errorf("expected called %d, got %d", 2, value)
|
||||
}
|
||||
|
||||
// At a later time, invalidating can be done again.
|
||||
SetCapabilitiesGetNow(t, capabilities, func() time.Time {
|
||||
return time.Now().Add(2 * time.Minute)
|
||||
})
|
||||
|
||||
capabilities.InvalidateCapabilities(url)
|
||||
|
||||
if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); !found {
|
||||
t.Error("could not find value for \"foo\"")
|
||||
} else if value != expectedString {
|
||||
t.Errorf("expected value %s, got %s", expectedString, value)
|
||||
} else if cached {
|
||||
t.Errorf("expected direct response")
|
||||
}
|
||||
|
||||
if value := called.Load(); value != 3 {
|
||||
t.Errorf("expected called %d, got %d", 3, value)
|
||||
}
|
||||
}
|
||||
|
|
165
certificate_reloader.go
Normal file
165
certificate_reloader.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* 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 (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type CertificateReloader struct {
|
||||
certFile string
|
||||
certWatcher *FileWatcher
|
||||
|
||||
keyFile string
|
||||
keyWatcher *FileWatcher
|
||||
|
||||
certificate atomic.Pointer[tls.Certificate]
|
||||
|
||||
reloadCounter atomic.Uint64
|
||||
}
|
||||
|
||||
func NewCertificateReloader(certFile string, keyFile string) (*CertificateReloader, error) {
|
||||
pair, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load certificate / key: %w", err)
|
||||
}
|
||||
|
||||
reloader := &CertificateReloader{
|
||||
certFile: certFile,
|
||||
keyFile: keyFile,
|
||||
}
|
||||
reloader.certificate.Store(&pair)
|
||||
reloader.certWatcher, err = NewFileWatcher(certFile, reloader.reload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reloader.keyWatcher, err = NewFileWatcher(keyFile, reloader.reload)
|
||||
if err != nil {
|
||||
reloader.certWatcher.Close() // nolint
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reloader, nil
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) Close() {
|
||||
r.keyWatcher.Close()
|
||||
r.certWatcher.Close()
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) reload(filename string) {
|
||||
log.Printf("reloading certificate from %s with %s", r.certFile, r.keyFile)
|
||||
pair, err := tls.LoadX509KeyPair(r.certFile, r.keyFile)
|
||||
if err != nil {
|
||||
log.Printf("could not load certificate / key: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.certificate.Store(&pair)
|
||||
r.reloadCounter.Add(1)
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) getCertificate() (*tls.Certificate, error) {
|
||||
return r.certificate.Load(), nil
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return r.getCertificate()
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) GetClientCertificate(i *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||
return r.getCertificate()
|
||||
}
|
||||
|
||||
func (r *CertificateReloader) GetReloadCounter() uint64 {
|
||||
return r.reloadCounter.Load()
|
||||
}
|
||||
|
||||
type CertPoolReloader struct {
|
||||
certFile string
|
||||
certWatcher *FileWatcher
|
||||
|
||||
pool atomic.Pointer[x509.CertPool]
|
||||
|
||||
reloadCounter atomic.Uint64
|
||||
}
|
||||
|
||||
func loadCertPool(filename string) (*x509.CertPool, error) {
|
||||
cert, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(cert) {
|
||||
return nil, fmt.Errorf("invalid CA in %s: %w", filename, err)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func NewCertPoolReloader(certFile string) (*CertPoolReloader, error) {
|
||||
pool, err := loadCertPool(certFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reloader := &CertPoolReloader{
|
||||
certFile: certFile,
|
||||
}
|
||||
reloader.pool.Store(pool)
|
||||
reloader.certWatcher, err = NewFileWatcher(certFile, reloader.reload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reloader, nil
|
||||
}
|
||||
|
||||
func (r *CertPoolReloader) Close() {
|
||||
r.certWatcher.Close()
|
||||
}
|
||||
|
||||
func (r *CertPoolReloader) reload(filename string) {
|
||||
log.Printf("reloading certificate pool from %s", r.certFile)
|
||||
pool, err := loadCertPool(r.certFile)
|
||||
if err != nil {
|
||||
log.Printf("could not load certificate pool: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.pool.Store(pool)
|
||||
r.reloadCounter.Add(1)
|
||||
}
|
||||
|
||||
func (r *CertPoolReloader) GetCertPool() *x509.CertPool {
|
||||
return r.pool.Load()
|
||||
}
|
||||
|
||||
func (r *CertPoolReloader) GetReloadCounter() uint64 {
|
||||
return r.reloadCounter.Load()
|
||||
}
|
62
certificate_reloader_test.go
Normal file
62
certificate_reloader_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 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"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func UpdateCertificateCheckIntervalForTest(t *testing.T, interval time.Duration) {
|
||||
t.Helper()
|
||||
// Make sure test is not executed with "t.Parallel()"
|
||||
t.Setenv("PARALLEL_CHECK", "1")
|
||||
old := deduplicateWatchEvents.Load()
|
||||
t.Cleanup(func() {
|
||||
deduplicateWatchEvents.Store(old)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
62
channel_waiter.go
Normal file
62
channel_waiter.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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 ChannelWaiters struct {
|
||||
mu sync.RWMutex
|
||||
id uint64
|
||||
waiters map[uint64]chan struct{}
|
||||
}
|
||||
|
||||
func (w *ChannelWaiters) Wakeup() {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
for _, ch := range w.waiters {
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
// Receiver is still processing previous wakeup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ChannelWaiters) Add(ch chan struct{}) uint64 {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.waiters == nil {
|
||||
w.waiters = make(map[uint64]chan struct{})
|
||||
}
|
||||
id := w.id
|
||||
w.id++
|
||||
w.waiters[id] = ch
|
||||
return id
|
||||
}
|
||||
|
||||
func (w *ChannelWaiters) Remove(id uint64) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
delete(w.waiters, id)
|
||||
}
|
66
channel_waiter_test.go
Normal file
66
channel_waiter_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChannelWaiters(t *testing.T) {
|
||||
var waiters ChannelWaiters
|
||||
|
||||
ch1 := make(chan struct{}, 1)
|
||||
id1 := waiters.Add(ch1)
|
||||
defer waiters.Remove(id1)
|
||||
|
||||
ch2 := make(chan struct{}, 1)
|
||||
id2 := waiters.Add(ch2)
|
||||
defer waiters.Remove(id2)
|
||||
|
||||
waiters.Wakeup()
|
||||
<-ch1
|
||||
<-ch2
|
||||
|
||||
select {
|
||||
case <-ch1:
|
||||
t.Error("should have not received another event")
|
||||
case <-ch2:
|
||||
t.Error("should have not received another event")
|
||||
default:
|
||||
}
|
||||
|
||||
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:
|
||||
t.Error("should have not received another event")
|
||||
default:
|
||||
}
|
||||
}
|
250
client.go
250
client.go
|
@ -23,14 +23,16 @@ package signaling
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mailru/easyjson"
|
||||
|
@ -93,30 +95,59 @@ type WritableClientMessage interface {
|
|||
CloseAfterSend(session Session) bool
|
||||
}
|
||||
|
||||
type HandlerClient interface {
|
||||
Context() context.Context
|
||||
RemoteAddr() string
|
||||
Country() string
|
||||
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
|
||||
SendMessage(message WritableClientMessage) bool
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
type ClientHandler interface {
|
||||
OnClosed(HandlerClient)
|
||||
OnMessageReceived(HandlerClient, []byte)
|
||||
OnRTTReceived(HandlerClient, time.Duration)
|
||||
}
|
||||
|
||||
type ClientGeoIpHandler interface {
|
||||
OnLookupCountry(HandlerClient) string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
conn *websocket.Conn
|
||||
addr string
|
||||
agent string
|
||||
closed uint32
|
||||
closed atomic.Int32
|
||||
country *string
|
||||
logRTT bool
|
||||
|
||||
session unsafe.Pointer
|
||||
handlerMu sync.RWMutex
|
||||
handler ClientHandler
|
||||
|
||||
session atomic.Pointer[Session]
|
||||
sessionId atomic.Pointer[string]
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
closeChan chan bool
|
||||
messagesDone sync.WaitGroup
|
||||
messageChan chan *bytes.Buffer
|
||||
messageProcessing uint32
|
||||
|
||||
OnLookupCountry func(*Client) string
|
||||
OnClosed func(*Client)
|
||||
OnMessageReceived func(*Client, []byte)
|
||||
OnRTTReceived func(*Client, time.Duration)
|
||||
closer *Closer
|
||||
closeOnce sync.Once
|
||||
messagesDone chan struct{}
|
||||
messageChan chan *bytes.Buffer
|
||||
}
|
||||
|
||||
func NewClient(conn *websocket.Conn, remoteAddress string, agent string) (*Client, error) {
|
||||
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"
|
||||
|
@ -125,47 +156,82 @@ func NewClient(conn *websocket.Conn, remoteAddress string, agent string) (*Clien
|
|||
if agent == "" {
|
||||
agent = "unknown user agent"
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
conn: conn,
|
||||
addr: remoteAddress,
|
||||
ctx: ctx,
|
||||
agent: agent,
|
||||
logRTT: true,
|
||||
|
||||
closeChan: make(chan bool, 1),
|
||||
messageChan: make(chan *bytes.Buffer, 16),
|
||||
|
||||
OnLookupCountry: func(client *Client) string { return unknownCountry },
|
||||
OnClosed: func(client *Client) {},
|
||||
OnMessageReceived: func(client *Client, data []byte) {},
|
||||
OnRTTReceived: func(client *Client, rtt time.Duration) {},
|
||||
}
|
||||
client.SetConn(conn, remoteAddress, handler)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetConn(conn *websocket.Conn, remoteAddress string) {
|
||||
func (c *Client) SetConn(conn *websocket.Conn, remoteAddress string, handler ClientHandler) {
|
||||
c.conn = conn
|
||||
c.addr = remoteAddress
|
||||
c.closeChan = make(chan bool, 1)
|
||||
c.SetHandler(handler)
|
||||
c.closer = NewCloser()
|
||||
c.messageChan = make(chan *bytes.Buffer, 16)
|
||||
c.OnLookupCountry = func(client *Client) string { return unknownCountry }
|
||||
c.OnClosed = func(client *Client) {}
|
||||
c.OnMessageReceived = func(client *Client, data []byte) {}
|
||||
c.messagesDone = make(chan struct{})
|
||||
}
|
||||
|
||||
func (c *Client) SetHandler(handler ClientHandler) {
|
||||
c.handlerMu.Lock()
|
||||
defer c.handlerMu.Unlock()
|
||||
c.handler = handler
|
||||
}
|
||||
|
||||
func (c *Client) getHandler() ClientHandler {
|
||||
c.handlerMu.RLock()
|
||||
defer c.handlerMu.RUnlock()
|
||||
return c.handler
|
||||
}
|
||||
|
||||
func (c *Client) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
return atomic.LoadUint32(&c.closed) == 0
|
||||
return c.closed.Load() == 0
|
||||
}
|
||||
|
||||
func (c *Client) IsAuthenticated() bool {
|
||||
return c.GetSession() != nil
|
||||
}
|
||||
|
||||
func (c *Client) GetSession() *ClientSession {
|
||||
return (*ClientSession)(atomic.LoadPointer(&c.session))
|
||||
func (c *Client) GetSession() Session {
|
||||
session := c.session.Load()
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return *session
|
||||
}
|
||||
|
||||
func (c *Client) SetSession(session *ClientSession) {
|
||||
atomic.StorePointer(&c.session, unsafe.Pointer(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) {
|
||||
c.sessionId.Store(&sessionId)
|
||||
}
|
||||
|
||||
func (c *Client) GetSessionId() string {
|
||||
sessionId := c.sessionId.Load()
|
||||
if sessionId == nil {
|
||||
session := c.GetSession()
|
||||
if session == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return session.PublicId()
|
||||
}
|
||||
|
||||
return *sessionId
|
||||
}
|
||||
|
||||
func (c *Client) RemoteAddr() string {
|
||||
|
@ -178,7 +244,12 @@ func (c *Client) UserAgent() string {
|
|||
|
||||
func (c *Client) Country() string {
|
||||
if c.country == nil {
|
||||
country := c.OnLookupCountry(c)
|
||||
var country string
|
||||
if handler, ok := c.getHandler().(ClientGeoIpHandler); ok {
|
||||
country = handler.OnLookupCountry(c)
|
||||
} else {
|
||||
country = unknownCountry
|
||||
}
|
||||
c.country = &country
|
||||
}
|
||||
|
||||
|
@ -186,38 +257,36 @@ func (c *Client) Country() string {
|
|||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) {
|
||||
if c.closed.Load() >= 2 {
|
||||
// Prevent reentrant call in case this was the second closing
|
||||
// step. Would otherwise deadlock in the "Once.Do" call path
|
||||
// through "Hub.processUnregister" (which calls "Close" again).
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // nolint
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if atomic.LoadUint32(&c.messageProcessing) == 1 {
|
||||
// Defer closing
|
||||
atomic.StoreUint32(&c.closed, 2)
|
||||
return
|
||||
}
|
||||
|
||||
c.doClose()
|
||||
c.closeOnce.Do(func() {
|
||||
c.doClose()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) doClose() {
|
||||
c.closeChan <- true
|
||||
c.messagesDone.Wait()
|
||||
closed := c.closed.Add(1)
|
||||
if closed == 1 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn != nil {
|
||||
c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // nolint
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
} else if closed == 2 {
|
||||
// Both the read pump and message processing must be finished before closing.
|
||||
c.closer.Close()
|
||||
<-c.messagesDone
|
||||
|
||||
c.OnClosed(c)
|
||||
c.SetSession(nil)
|
||||
|
||||
c.mu.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
c.getHandler().OnClosed(c)
|
||||
c.SetSession(nil)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) SendError(e *Error) bool {
|
||||
|
@ -235,12 +304,14 @@ func (c *Client) SendByeResponse(message *ClientMessage) bool {
|
|||
func (c *Client) SendByeResponseWithReason(message *ClientMessage, reason string) bool {
|
||||
response := &ServerMessage{
|
||||
Type: "bye",
|
||||
Bye: &ByeServerMessage{},
|
||||
}
|
||||
if message != nil {
|
||||
response.Id = message.Id
|
||||
}
|
||||
if reason != "" {
|
||||
if response.Bye == nil {
|
||||
response.Bye = &ByeServerMessage{}
|
||||
}
|
||||
response.Bye.Reason = reason
|
||||
}
|
||||
return c.SendMessage(response)
|
||||
|
@ -252,10 +323,12 @@ func (c *Client) SendMessage(message WritableClientMessage) bool {
|
|||
|
||||
func (c *Client) ReadPump() {
|
||||
defer func() {
|
||||
c.Close()
|
||||
close(c.messageChan)
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
go c.processMessages()
|
||||
|
||||
addr := c.RemoteAddr()
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
|
@ -276,29 +349,30 @@ func (c *Client) ReadPump() {
|
|||
rtt := now.Sub(time.Unix(0, ts))
|
||||
if c.logRTT {
|
||||
rtt_ms := rtt.Nanoseconds() / time.Millisecond.Nanoseconds()
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Client %s has RTT of %d ms (%s)", session.PublicId(), rtt_ms, rtt)
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.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.OnRTTReceived(c, rtt)
|
||||
c.getHandler().OnRTTReceived(c, rtt)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
go c.processMessages()
|
||||
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint
|
||||
messageType, reader, err := conn.NextReader()
|
||||
if err != nil {
|
||||
if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err,
|
||||
// Gorilla websocket hides the original net.Error, so also compare error messages
|
||||
if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), net.ErrClosed.Error()) {
|
||||
break
|
||||
} else if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err,
|
||||
websocket.CloseNormalClosure,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseNoStatusReceived) {
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Error reading from client %s: %v", session.PublicId(), err)
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Error reading from client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Error reading from %s: %v", addr, err)
|
||||
}
|
||||
|
@ -307,8 +381,8 @@ func (c *Client) ReadPump() {
|
|||
}
|
||||
|
||||
if messageType != websocket.TextMessage {
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Unsupported message type %v from client %s", messageType, session.PublicId())
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Unsupported message type %v from client %s", messageType, sessionId)
|
||||
} else {
|
||||
log.Printf("Unsupported message type %v from %s", messageType, addr)
|
||||
}
|
||||
|
@ -320,8 +394,8 @@ func (c *Client) ReadPump() {
|
|||
decodeBuffer.Reset()
|
||||
if _, err := decodeBuffer.ReadFrom(reader); err != nil {
|
||||
bufferPool.Put(decodeBuffer)
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Error reading message from client %s: %v", session.PublicId(), err)
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Error reading message from client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Error reading message from %s: %v", addr, err)
|
||||
}
|
||||
|
@ -329,12 +403,11 @@ func (c *Client) ReadPump() {
|
|||
}
|
||||
|
||||
// Stop processing if the client was closed.
|
||||
if atomic.LoadUint32(&c.closed) != 0 {
|
||||
if !c.IsConnected() {
|
||||
bufferPool.Put(decodeBuffer)
|
||||
break
|
||||
}
|
||||
|
||||
c.messagesDone.Add(1)
|
||||
c.messageChan <- decodeBuffer
|
||||
}
|
||||
}
|
||||
|
@ -346,16 +419,12 @@ func (c *Client) processMessages() {
|
|||
break
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&c.messageProcessing, 1)
|
||||
c.OnMessageReceived(c, buffer.Bytes())
|
||||
atomic.StoreUint32(&c.messageProcessing, 0)
|
||||
c.messagesDone.Done()
|
||||
c.getHandler().OnMessageReceived(c, buffer.Bytes())
|
||||
bufferPool.Put(buffer)
|
||||
}
|
||||
|
||||
if atomic.LoadUint32(&c.closed) == 2 {
|
||||
c.doClose()
|
||||
}
|
||||
close(c.messagesDone)
|
||||
c.doClose()
|
||||
}
|
||||
|
||||
func (c *Client) writeInternal(message json.Marshaler) bool {
|
||||
|
@ -379,8 +448,8 @@ func (c *Client) writeInternal(message json.Marshaler) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Could not send message %+v to client %s: %v", message, session.PublicId(), err)
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.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)
|
||||
}
|
||||
|
@ -392,8 +461,8 @@ func (c *Client) writeInternal(message json.Marshaler) bool {
|
|||
close:
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil {
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Could not send close message to client %s: %v", session.PublicId(), err)
|
||||
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)
|
||||
}
|
||||
|
@ -419,8 +488,8 @@ func (c *Client) writeError(e error) bool { // nolint
|
|||
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 session := c.GetSession(); session != nil {
|
||||
log.Printf("Could not send close message to client %s: %v", session.PublicId(), err)
|
||||
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)
|
||||
}
|
||||
|
@ -451,7 +520,6 @@ func (c *Client) writeMessageLocked(message WritableClientMessage) bool {
|
|||
go session.Close()
|
||||
}
|
||||
go c.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -468,8 +536,8 @@ func (c *Client) sendPing() bool {
|
|||
msg := strconv.FormatInt(now, 10)
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil {
|
||||
if session := c.GetSession(); session != nil {
|
||||
log.Printf("Could not send ping to client %s: %v", session.PublicId(), err)
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
log.Printf("Could not send ping to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
log.Printf("Could not send ping to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
|
@ -493,7 +561,7 @@ func (c *Client) WritePump() {
|
|||
if !c.sendPing() {
|
||||
return
|
||||
}
|
||||
case <-c.closeChan:
|
||||
case <-c.closer.C:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,8 +81,8 @@ const (
|
|||
)
|
||||
|
||||
type Stats struct {
|
||||
numRecvMessages uint64
|
||||
numSentMessages uint64
|
||||
numRecvMessages atomic.Uint64
|
||||
numSentMessages atomic.Uint64
|
||||
resetRecvMessages uint64
|
||||
resetSentMessages uint64
|
||||
|
||||
|
@ -90,8 +90,8 @@ type Stats struct {
|
|||
}
|
||||
|
||||
func (s *Stats) reset(start time.Time) {
|
||||
s.resetRecvMessages = atomic.AddUint64(&s.numRecvMessages, 0)
|
||||
s.resetSentMessages = atomic.AddUint64(&s.numSentMessages, 0)
|
||||
s.resetRecvMessages = s.numRecvMessages.Load()
|
||||
s.resetSentMessages = s.numSentMessages.Load()
|
||||
s.start = start
|
||||
}
|
||||
|
||||
|
@ -103,9 +103,9 @@ func (s *Stats) Log() {
|
|||
return
|
||||
}
|
||||
|
||||
totalSentMessages := atomic.AddUint64(&s.numSentMessages, 0)
|
||||
totalSentMessages := s.numSentMessages.Load()
|
||||
sentMessages := totalSentMessages - s.resetSentMessages
|
||||
totalRecvMessages := atomic.AddUint64(&s.numRecvMessages, 0)
|
||||
totalRecvMessages := s.numRecvMessages.Load()
|
||||
recvMessages := totalRecvMessages - s.resetRecvMessages
|
||||
log.Printf("Stats: sent=%d (%d/sec), recv=%d (%d/sec), delta=%d",
|
||||
totalSentMessages, sentMessages/perSec,
|
||||
|
@ -125,9 +125,9 @@ type SignalingClient struct {
|
|||
conn *websocket.Conn
|
||||
|
||||
stats *Stats
|
||||
closed uint32
|
||||
closed atomic.Bool
|
||||
|
||||
stopChan chan bool
|
||||
stopChan chan struct{}
|
||||
|
||||
lock sync.Mutex
|
||||
privateSessionId string
|
||||
|
@ -149,7 +149,7 @@ func NewSignalingClient(cookie *securecookie.SecureCookie, url string, stats *St
|
|||
|
||||
stats: stats,
|
||||
|
||||
stopChan: make(chan bool),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
doneWg.Add(2)
|
||||
go func() {
|
||||
|
@ -164,15 +164,12 @@ func NewSignalingClient(cookie *securecookie.SecureCookie, url string, stats *St
|
|||
}
|
||||
|
||||
func (c *SignalingClient) Close() {
|
||||
if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) {
|
||||
if !c.closed.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
|
||||
// Signal writepump to terminate
|
||||
select {
|
||||
case c.stopChan <- true:
|
||||
default:
|
||||
}
|
||||
close(c.stopChan)
|
||||
|
||||
c.lock.Lock()
|
||||
c.publicSessionId = ""
|
||||
|
@ -200,7 +197,7 @@ func (c *SignalingClient) Send(message *signaling.ClientMessage) {
|
|||
}
|
||||
|
||||
func (c *SignalingClient) processMessage(message *signaling.ServerMessage) {
|
||||
atomic.AddUint64(&c.stats.numRecvMessages, 1)
|
||||
c.stats.numRecvMessages.Add(1)
|
||||
switch message.Type {
|
||||
case "hello":
|
||||
c.processHelloMessage(message)
|
||||
|
@ -251,7 +248,7 @@ func (c *SignalingClient) PublicSessionId() string {
|
|||
|
||||
func (c *SignalingClient) processMessageMessage(message *signaling.ServerMessage) {
|
||||
var msg MessagePayload
|
||||
if err := json.Unmarshal(*message.Message.Data, &msg); err != nil {
|
||||
if err := json.Unmarshal(message.Message.Data, &msg); err != nil {
|
||||
log.Println("Error in unmarshal", err)
|
||||
return
|
||||
}
|
||||
|
@ -337,7 +334,7 @@ func (c *SignalingClient) writeInternal(message *signaling.ClientMessage) bool {
|
|||
}
|
||||
|
||||
writer.Close()
|
||||
atomic.AddUint64(&c.stats.numSentMessages, 1)
|
||||
c.stats.numSentMessages.Add(1)
|
||||
return true
|
||||
|
||||
close:
|
||||
|
@ -386,7 +383,7 @@ func (c *SignalingClient) SendMessages(clients []*SignalingClient) {
|
|||
sessionIds[c] = c.PublicSessionId()
|
||||
}
|
||||
|
||||
for atomic.LoadUint32(&c.closed) == 0 {
|
||||
for !c.closed.Load() {
|
||||
now := time.Now()
|
||||
|
||||
sender := c
|
||||
|
@ -407,7 +404,7 @@ func (c *SignalingClient) SendMessages(clients []*SignalingClient) {
|
|||
Type: "session",
|
||||
SessionId: sessionIds[recipient],
|
||||
},
|
||||
Data: (*json.RawMessage)(&data),
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
sender.Send(msg)
|
||||
|
@ -464,7 +461,7 @@ func registerAuthHandler(router *mux.Router) {
|
|||
StatusCode: http.StatusOK,
|
||||
Message: http.StatusText(http.StatusOK),
|
||||
},
|
||||
Data: &rawdata,
|
||||
Data: rawdata,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -603,10 +600,10 @@ func main() {
|
|||
request := &signaling.ClientMessage{
|
||||
Type: "hello",
|
||||
Hello: &signaling.HelloClientMessage{
|
||||
Version: signaling.HelloVersion,
|
||||
Auth: signaling.HelloClientMessageAuth{
|
||||
Version: signaling.HelloVersionV1,
|
||||
Auth: &signaling.HelloClientMessageAuth{
|
||||
Url: backendUrl + "/auth",
|
||||
Params: &json.RawMessage{'{', '}'},
|
||||
Params: json.RawMessage("{}"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
628
clientsession.go
628
clientsession.go
File diff suppressed because it is too large
Load diff
|
@ -117,6 +117,7 @@ func Test_permissionsEqual(t *testing.T) {
|
|||
for idx, test := range tests {
|
||||
test := test
|
||||
t.Run(strconv.Itoa(idx), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
equal := permissionsEqual(test.a, test.b)
|
||||
if equal != test.equal {
|
||||
t.Errorf("Expected %+v to be %s to %+v but was %s", test.a, equalStrings[test.equal], test.b, equalStrings[equal])
|
||||
|
@ -126,12 +127,17 @@ func Test_permissionsEqual(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBandwidth_Client(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
hub, _, _, server := CreateHubForTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
mcu, err := NewTestMCU()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := mcu.Start(); err != nil {
|
||||
} else if err := mcu.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer mcu.Stop()
|
||||
|
@ -145,9 +151,6 @@ func TestBandwidth_Client(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
hello, err := client.RunUntilHello(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -198,6 +201,8 @@ func TestBandwidth_Client(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBandwidth_Backend(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
hub, _, _, server := CreateHubWithMultipleBackendsForTest(t)
|
||||
|
||||
u, err := url.Parse(server.URL + "/one")
|
||||
|
@ -212,33 +217,33 @@ func TestBandwidth_Backend(t *testing.T) {
|
|||
backend.maxScreenBitrate = 1000
|
||||
backend.maxStreamBitrate = 2000
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
mcu, err := NewTestMCU()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := mcu.Start(); err != nil {
|
||||
} else if err := mcu.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer mcu.Stop()
|
||||
|
||||
hub.SetMcu(mcu)
|
||||
|
||||
streamTypes := []string{
|
||||
streamTypeVideo,
|
||||
streamTypeScreen,
|
||||
streamTypes := []StreamType{
|
||||
StreamTypeVideo,
|
||||
StreamTypeScreen,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
for _, streamType := range streamTypes {
|
||||
t.Run(streamType, func(t *testing.T) {
|
||||
t.Run(string(streamType), func(t *testing.T) {
|
||||
client := NewTestClient(t, server, hub)
|
||||
defer client.CloseWithBye()
|
||||
|
||||
params := TestBackendClientAuthParams{
|
||||
UserId: testDefaultUserId,
|
||||
}
|
||||
if err := client.SendHelloParams(server.URL+"/one", "client", params); err != nil {
|
||||
if err := client.SendHelloParams(server.URL+"/one", HelloVersionV1, "client", nil, params); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -268,7 +273,7 @@ func TestBandwidth_Backend(t *testing.T) {
|
|||
}, MessageClientMessageData{
|
||||
Type: "offer",
|
||||
Sid: "54321",
|
||||
RoomType: streamType,
|
||||
RoomType: string(streamType),
|
||||
Bitrate: bitrate,
|
||||
Payload: map[string]interface{}{
|
||||
"sdp": MockSdpOfferAudioAndVideo,
|
||||
|
@ -287,7 +292,7 @@ func TestBandwidth_Backend(t *testing.T) {
|
|||
}
|
||||
|
||||
var expectBitrate int
|
||||
if streamType == streamTypeVideo {
|
||||
if streamType == StreamTypeVideo {
|
||||
expectBitrate = backend.maxStreamBitrate
|
||||
} else {
|
||||
expectBitrate = backend.maxScreenBitrate
|
||||
|
|
47
closer.go
Normal file
47
closer.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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/atomic"
|
||||
)
|
||||
|
||||
type Closer struct {
|
||||
closed atomic.Bool
|
||||
C chan struct{}
|
||||
}
|
||||
|
||||
func NewCloser() *Closer {
|
||||
return &Closer{
|
||||
C: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Closer) IsClosed() bool {
|
||||
return c.closed.Load()
|
||||
}
|
||||
|
||||
func (c *Closer) Close() {
|
||||
if c.closed.CompareAndSwap(false, true) {
|
||||
close(c.C)
|
||||
}
|
||||
}
|
62
closer_test.go
Normal file
62
closer_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloserMulti(t *testing.T) {
|
||||
closer := NewCloser()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
count := 10
|
||||
for i := 0; i < count; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-closer.C
|
||||
}()
|
||||
}
|
||||
|
||||
if closer.IsClosed() {
|
||||
t.Error("should not be closed")
|
||||
}
|
||||
closer.Close()
|
||||
if !closer.IsClosed() {
|
||||
t.Error("should be closed")
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCloserCloseBeforeWait(t *testing.T) {
|
||||
closer := NewCloser()
|
||||
closer.Close()
|
||||
if !closer.IsClosed() {
|
||||
t.Error("should be closed")
|
||||
}
|
||||
<-closer.C
|
||||
if !closer.IsClosed() {
|
||||
t.Error("should be closed")
|
||||
}
|
||||
}
|
87
config.go
Normal file
87
config.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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 (
|
||||
"errors"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
)
|
||||
|
||||
var (
|
||||
searchVarsRegexp = regexp.MustCompile(`\$\([A-Za-z][A-Za-z0-9_]*\)`)
|
||||
)
|
||||
|
||||
func replaceEnvVars(s string) string {
|
||||
return searchVarsRegexp.ReplaceAllStringFunc(s, func(name string) string {
|
||||
name = name[2 : len(name)-1]
|
||||
value, found := os.LookupEnv(name)
|
||||
if !found {
|
||||
return name
|
||||
}
|
||||
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
// GetStringOptionWithEnv will get the string option and resolve any environment
|
||||
// variable references in the form "$(VAR)".
|
||||
func GetStringOptionWithEnv(config *goconf.ConfigFile, section string, option string) (string, error) {
|
||||
value, err := config.GetString(section, option)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
value = replaceEnvVars(value)
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func GetStringOptions(config *goconf.ConfigFile, section string, ignoreErrors bool) (map[string]string, error) {
|
||||
options, _ := config.GetOptions(section)
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
for _, option := range options {
|
||||
value, err := GetStringOptionWithEnv(config, section, option)
|
||||
if err != nil {
|
||||
if ignoreErrors {
|
||||
continue
|
||||
}
|
||||
|
||||
var ge goconf.GetError
|
||||
if errors.As(err, &ge) && ge.Reason == goconf.OptionNotFound {
|
||||
// Skip options from "default" section.
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[option] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
92
config_test.go
Normal file
92
config_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
)
|
||||
|
||||
func TestStringOptions(t *testing.T) {
|
||||
t.Setenv("FOO", "foo")
|
||||
expected := map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"foo": "http://foo/1",
|
||||
}
|
||||
config := goconf.NewConfigFile()
|
||||
for k, v := range expected {
|
||||
if k == "foo" {
|
||||
config.AddOption("foo", k, "http://$(FOO)/1")
|
||||
} else {
|
||||
config.AddOption("foo", k, v)
|
||||
}
|
||||
}
|
||||
config.AddOption("default", "three", "3")
|
||||
|
||||
options, err := GetStringOptions(config, "foo", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, options) {
|
||||
t.Errorf("expected %+v, got %+v", expected, options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringOptionWithEnv(t *testing.T) {
|
||||
t.Setenv("FOO", "foo")
|
||||
t.Setenv("BAR", "")
|
||||
t.Setenv("BA_R", "bar")
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("test", "foo", "http://$(FOO)/1")
|
||||
config.AddOption("test", "bar", "http://$(BAR)/2")
|
||||
config.AddOption("test", "bar2", "http://$(BA_R)/3")
|
||||
config.AddOption("test", "baz", "http://$(BAZ)/4")
|
||||
config.AddOption("test", "inv1", "http://$(FOO")
|
||||
config.AddOption("test", "inv2", "http://$FOO)")
|
||||
config.AddOption("test", "inv3", "http://$((FOO)")
|
||||
config.AddOption("test", "inv4", "http://$(F.OO)")
|
||||
|
||||
expected := map[string]string{
|
||||
"foo": "http://foo/1",
|
||||
"bar": "http:///2",
|
||||
"bar2": "http://bar/3",
|
||||
"baz": "http://BAZ/4",
|
||||
"inv1": "http://$(FOO",
|
||||
"inv2": "http://$FOO)",
|
||||
"inv3": "http://$((FOO)",
|
||||
"inv4": "http://$(F.OO)",
|
||||
}
|
||||
for k, v := range expected {
|
||||
value, err := GetStringOptionWithEnv(config, "test", k)
|
||||
if err != nil {
|
||||
t.Errorf("expected value for %s, got %s", k, err)
|
||||
} else if value != v {
|
||||
t.Errorf("expected value %s for %s, got %s", v, k, value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package signaling
|
||||
|
||||
// This file has been automatically generated, do not modify.
|
||||
// Source: https://datahub.io/core/country-codes/r/country-codes.json
|
||||
// Source: https://github.com/datasets/country-codes/raw/master/data/country-codes.csv
|
||||
|
||||
var (
|
||||
ContinentMap = map[string][]string{
|
||||
|
|
|
@ -33,8 +33,7 @@ import (
|
|||
// their order.
|
||||
type DeferredExecutor struct {
|
||||
queue chan func()
|
||||
closeChan chan bool
|
||||
closed chan bool
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
|
@ -43,28 +42,24 @@ func NewDeferredExecutor(queueSize int) *DeferredExecutor {
|
|||
queueSize = 0
|
||||
}
|
||||
result := &DeferredExecutor{
|
||||
queue: make(chan func(), queueSize),
|
||||
closeChan: make(chan bool, 1),
|
||||
closed: make(chan bool, 1),
|
||||
queue: make(chan func(), queueSize),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
go result.run()
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *DeferredExecutor) run() {
|
||||
loop:
|
||||
defer close(e.closed)
|
||||
|
||||
for {
|
||||
select {
|
||||
case f := <-e.queue:
|
||||
if f == nil {
|
||||
break loop
|
||||
}
|
||||
f()
|
||||
case <-e.closeChan:
|
||||
break loop
|
||||
f := <-e.queue
|
||||
if f == nil {
|
||||
break
|
||||
}
|
||||
|
||||
f()
|
||||
}
|
||||
e.closed <- true
|
||||
}
|
||||
|
||||
func getFunctionName(i interface{}) string {
|
||||
|
@ -83,14 +78,9 @@ func (e *DeferredExecutor) Execute(f func()) {
|
|||
}
|
||||
|
||||
func (e *DeferredExecutor) Close() {
|
||||
select {
|
||||
case e.closeChan <- true:
|
||||
e.closeOnce.Do(func() {
|
||||
close(e.queue)
|
||||
})
|
||||
default:
|
||||
// Already closed.
|
||||
}
|
||||
e.closeOnce.Do(func() {
|
||||
close(e.queue)
|
||||
})
|
||||
}
|
||||
|
||||
func (e *DeferredExecutor) waitForStop() {
|
||||
|
|
|
@ -35,6 +35,7 @@ func TestDeferredExecutor_MultiClose(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeferredExecutor_QueueSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := NewDeferredExecutor(0)
|
||||
defer e.waitForStop()
|
||||
defer e.Close()
|
||||
|
@ -69,13 +70,13 @@ func TestDeferredExecutor_Order(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
done := make(chan struct{})
|
||||
for x := 0; x < 10; x++ {
|
||||
e.Execute(getFunc(x))
|
||||
}
|
||||
|
||||
e.Execute(func() {
|
||||
done <- true
|
||||
close(done)
|
||||
})
|
||||
<-done
|
||||
|
||||
|
@ -90,16 +91,17 @@ func TestDeferredExecutor_CloseFromFunc(t *testing.T) {
|
|||
e := NewDeferredExecutor(64)
|
||||
defer e.waitForStop()
|
||||
|
||||
done := make(chan bool)
|
||||
done := make(chan struct{})
|
||||
e.Execute(func() {
|
||||
defer close(done)
|
||||
e.Close()
|
||||
done <- true
|
||||
})
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestDeferredExecutor_DeferAfterClose(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
e := NewDeferredExecutor(64)
|
||||
defer e.waitForStop()
|
||||
|
||||
|
@ -109,3 +111,12 @@ func TestDeferredExecutor_DeferAfterClose(t *testing.T) {
|
|||
t.Error("method should not have been called")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeferredExecutor_WaitForStopTwice(t *testing.T) {
|
||||
e := NewDeferredExecutor(64)
|
||||
defer e.waitForStop()
|
||||
|
||||
e.Close()
|
||||
|
||||
e.waitForStop()
|
||||
}
|
||||
|
|
32
dist/init/systemd/signaling.service
vendored
32
dist/init/systemd/signaling.service
vendored
|
@ -7,5 +7,37 @@ User=signaling
|
|||
Group=signaling
|
||||
Restart=on-failure
|
||||
|
||||
# Makes sure that /etc/signaling is owned by this service
|
||||
ConfigurationDirectory=signaling
|
||||
|
||||
# Hardening - see systemd.exec(5)
|
||||
CapabilityBoundingSet=
|
||||
ExecPaths=/usr/bin/signaling /usr/lib
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
NoExecPaths=/
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateTmp=yes
|
||||
PrivateUsers=yes
|
||||
ProcSubset=pid
|
||||
ProtectClock=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectHome=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectProc=invisible
|
||||
ProtectSystem=strict
|
||||
RemoveIPC=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~ @privileged
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
5
dist/init/systemd/sysusers.d/signaling.conf
vendored
Normal file
5
dist/init/systemd/sysusers.d/signaling.conf
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# SPDX-FileCopyrightText: 2022 Andrea Pappacoda <andrea@pappacoda.it>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
u signaling - "nextcloud-spreed-signaling user"
|
343
dns_monitor.go
Normal file
343
dns_monitor.go
Normal file
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
lookupDnsMonitorIP = net.LookupIP
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDnsMonitorInterval = time.Second
|
||||
)
|
||||
|
||||
type DnsMonitorCallback = func(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP)
|
||||
|
||||
type DnsMonitorEntry struct {
|
||||
entry atomic.Pointer[dnsMonitorEntry]
|
||||
url string
|
||||
callback DnsMonitorCallback
|
||||
}
|
||||
|
||||
func (e *DnsMonitorEntry) URL() string {
|
||||
return e.url
|
||||
}
|
||||
|
||||
type dnsMonitorEntry struct {
|
||||
hostname string
|
||||
hostIP net.IP
|
||||
|
||||
mu sync.Mutex
|
||||
ips []net.IP
|
||||
entries map[*DnsMonitorEntry]bool
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) setIPs(ips []net.IP, fromIP bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
empty := len(e.ips) == 0
|
||||
if empty {
|
||||
// Simple case: initial lookup.
|
||||
if len(ips) > 0 {
|
||||
e.ips = ips
|
||||
e.runCallbacks(ips, ips, nil, nil)
|
||||
}
|
||||
return
|
||||
} else if fromIP {
|
||||
// No more updates possible for IP addresses.
|
||||
return
|
||||
} else if len(ips) == 0 {
|
||||
// Simple case: no records received from lookup.
|
||||
if !empty {
|
||||
removed := e.ips
|
||||
e.ips = nil
|
||||
e.runCallbacks(nil, nil, nil, removed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var newIPs []net.IP
|
||||
var addedIPs []net.IP
|
||||
var removedIPs []net.IP
|
||||
var keepIPs []net.IP
|
||||
for _, oldIP := range e.ips {
|
||||
found := false
|
||||
for idx, newIP := range ips {
|
||||
if oldIP.Equal(newIP) {
|
||||
ips = append(ips[:idx], ips[idx+1:]...)
|
||||
found = true
|
||||
keepIPs = append(keepIPs, oldIP)
|
||||
newIPs = append(newIPs, oldIP)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
removedIPs = append(removedIPs, oldIP)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
addedIPs = append(addedIPs, ips...)
|
||||
newIPs = append(newIPs, ips...)
|
||||
}
|
||||
e.ips = newIPs
|
||||
|
||||
if len(addedIPs) > 0 || len(removedIPs) > 0 {
|
||||
e.runCallbacks(newIPs, addedIPs, keepIPs, removedIPs)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) addEntry(entry *DnsMonitorEntry) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.entries[entry] = true
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) removeEntry(entry *DnsMonitorEntry) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
delete(e.entries, entry)
|
||||
return len(e.entries) == 0
|
||||
}
|
||||
|
||||
func (e *dnsMonitorEntry) 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
|
||||
|
||||
stopCtx context.Context
|
||||
stopFunc func()
|
||||
stopped chan struct{}
|
||||
|
||||
mu sync.RWMutex
|
||||
cond *sync.Cond
|
||||
hostnames map[string]*dnsMonitorEntry
|
||||
|
||||
hasRemoved atomic.Bool
|
||||
|
||||
// Can be overwritten from tests.
|
||||
checkHostnames func()
|
||||
}
|
||||
|
||||
func NewDnsMonitor(interval time.Duration) (*DnsMonitor, error) {
|
||||
if interval < 0 {
|
||||
interval = defaultDnsMonitorInterval
|
||||
}
|
||||
|
||||
stopCtx, stopFunc := context.WithCancel(context.Background())
|
||||
monitor := &DnsMonitor{
|
||||
interval: interval,
|
||||
|
||||
stopCtx: stopCtx,
|
||||
stopFunc: stopFunc,
|
||||
stopped: make(chan struct{}),
|
||||
|
||||
hostnames: make(map[string]*dnsMonitorEntry),
|
||||
}
|
||||
monitor.cond = sync.NewCond(&monitor.mu)
|
||||
monitor.checkHostnames = monitor.doCheckHostnames
|
||||
return monitor, nil
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Start() error {
|
||||
go m.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Stop() {
|
||||
m.stopFunc()
|
||||
m.cond.Signal()
|
||||
<-m.stopped
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Add(target string, callback DnsMonitorCallback) (*DnsMonitorEntry, error) {
|
||||
var hostname string
|
||||
if strings.Contains(target, "://") {
|
||||
// Full URL passed.
|
||||
parsed, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostname = parsed.Host
|
||||
} else {
|
||||
// Hostname only passed.
|
||||
hostname = target
|
||||
}
|
||||
if h, _, err := net.SplitHostPort(hostname); err == nil {
|
||||
hostname = h
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
e := &DnsMonitorEntry{
|
||||
url: target,
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
entry, found := m.hostnames[hostname]
|
||||
if !found {
|
||||
entry = &dnsMonitorEntry{
|
||||
hostname: hostname,
|
||||
hostIP: net.ParseIP(hostname),
|
||||
entries: make(map[*DnsMonitorEntry]bool),
|
||||
}
|
||||
m.hostnames[hostname] = entry
|
||||
}
|
||||
e.entry.Store(entry)
|
||||
entry.addEntry(e)
|
||||
m.cond.Signal()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) Remove(entry *DnsMonitorEntry) {
|
||||
oldEntry := entry.entry.Swap(nil)
|
||||
if oldEntry == nil {
|
||||
// Already removed.
|
||||
return
|
||||
}
|
||||
|
||||
locked := m.mu.TryLock()
|
||||
// Spin-lock for simple cases that resolve immediately to avoid deferred removal.
|
||||
for i := 0; !locked && i < 1000; i++ {
|
||||
time.Sleep(time.Nanosecond)
|
||||
locked = m.mu.TryLock()
|
||||
}
|
||||
if !locked {
|
||||
// Currently processing callbacks for this entry, need to defer removal.
|
||||
m.hasRemoved.Store(true)
|
||||
return
|
||||
}
|
||||
defer m.mu.Unlock()
|
||||
|
||||
e, found := m.hostnames[oldEntry.hostname]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
if e.removeEntry(entry) {
|
||||
delete(m.hostnames, e.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) clearRemoved() {
|
||||
if !m.hasRemoved.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
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 {
|
||||
delete(m.hostnames, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) waitForEntries() (waited bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for len(m.hostnames) == 0 && m.stopCtx.Err() == nil {
|
||||
m.cond.Wait()
|
||||
waited = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) run() {
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
defer close(m.stopped)
|
||||
|
||||
for {
|
||||
if m.waitForEntries() {
|
||||
ticker.Reset(m.interval)
|
||||
if m.stopCtx.Err() == nil {
|
||||
// Initial check when a new entry was added. More checks will be
|
||||
// triggered by the Ticker.
|
||||
m.checkHostnames()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.stopCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.checkHostnames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) doCheckHostnames() {
|
||||
m.clearRemoved()
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, entry := range m.hostnames {
|
||||
m.checkHostname(entry)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DnsMonitor) checkHostname(entry *dnsMonitorEntry) {
|
||||
if len(entry.hostIP) > 0 {
|
||||
entry.setIPs([]net.IP{entry.hostIP}, true)
|
||||
return
|
||||
}
|
||||
|
||||
ips, err := lookupDnsMonitorIP(entry.hostname)
|
||||
if err != nil {
|
||||
log.Printf("Could not lookup %s: %s", entry.hostname, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.setIPs(ips, false)
|
||||
}
|
428
dns_monitor_test.go
Normal file
428
dns_monitor_test.go
Normal file
|
@ -0,0 +1,428 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 {
|
||||
t.Helper()
|
||||
|
||||
monitor, err := NewDnsMonitor(interval)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
monitor.Stop()
|
||||
})
|
||||
|
||||
if err := monitor.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return monitor
|
||||
}
|
||||
|
||||
type dnsMonitorReceiverRecord struct {
|
||||
all []net.IP
|
||||
add []net.IP
|
||||
keep []net.IP
|
||||
remove []net.IP
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiverRecord) Equal(other *dnsMonitorReceiverRecord) 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 {
|
||||
return fmt.Sprintf("all=%v, add=%v, keep=%v, remove=%v", r.all, r.add, r.keep, r.remove)
|
||||
}
|
||||
|
||||
var (
|
||||
expectNone = &dnsMonitorReceiverRecord{}
|
||||
)
|
||||
|
||||
type dnsMonitorReceiver struct {
|
||||
sync.Mutex
|
||||
|
||||
t *testing.T
|
||||
expected *dnsMonitorReceiverRecord
|
||||
received *dnsMonitorReceiverRecord
|
||||
}
|
||||
|
||||
func newDnsMonitorReceiverForTest(t *testing.T) *dnsMonitorReceiver {
|
||||
return &dnsMonitorReceiver{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all, add, keep, remove []net.IP) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
received := &dnsMonitorReceiverRecord{
|
||||
all: all,
|
||||
add: add,
|
||||
keep: keep,
|
||||
remove: remove,
|
||||
}
|
||||
|
||||
expected := r.expected
|
||||
r.expected = nil
|
||||
if expected == expectNone {
|
||||
r.t.Errorf("expected no event, got %v", received)
|
||||
return
|
||||
}
|
||||
|
||||
if expected == nil {
|
||||
if r.received != nil && !r.received.Equal(received) {
|
||||
r.t.Errorf("already received %v, got %v", r.received, received)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !expected.Equal(received) {
|
||||
r.t.Errorf("expected %v, got %v", expected, received)
|
||||
}
|
||||
r.received = nil
|
||||
r.expected = nil
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) WaitForExpected(ctx context.Context) {
|
||||
r.t.Helper()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
ticker := time.NewTicker(time.Microsecond)
|
||||
abort := false
|
||||
for r.expected != nil && !abort {
|
||||
r.Unlock()
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-ctx.Done():
|
||||
r.t.Error(ctx.Err())
|
||||
abort = true
|
||||
}
|
||||
r.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) Expect(all, add, keep, remove []net.IP) {
|
||||
r.t.Helper()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.expected != nil && r.expected != expectNone {
|
||||
r.t.Errorf("didn't get previously expected %v", r.expected)
|
||||
}
|
||||
|
||||
expected := &dnsMonitorReceiverRecord{
|
||||
all: all,
|
||||
add: add,
|
||||
keep: keep,
|
||||
remove: remove,
|
||||
}
|
||||
if r.received != nil && r.received.Equal(expected) {
|
||||
r.received = nil
|
||||
return
|
||||
}
|
||||
|
||||
r.expected = expected
|
||||
}
|
||||
|
||||
func (r *dnsMonitorReceiver) ExpectNone() {
|
||||
r.t.Helper()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.expected != nil && r.expected != expectNone {
|
||||
r.t.Errorf("didn't get previously expected %v", r.expected)
|
||||
}
|
||||
|
||||
r.expected = expectNone
|
||||
}
|
||||
|
||||
func TestDnsMonitor(t *testing.T) {
|
||||
lookup := newMockDnsLookupForTest(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
|
||||
ip1 := net.ParseIP("192.168.0.1")
|
||||
ip2 := net.ParseIP("192.168.1.1")
|
||||
ip3 := net.ParseIP("10.1.2.3")
|
||||
ips1 := []net.IP{
|
||||
ip1,
|
||||
ip2,
|
||||
}
|
||||
lookup.Set("foo", ips1)
|
||||
|
||||
rec1 := newDnsMonitorReceiverForTest(t)
|
||||
rec1.Expect(ips1, ips1, nil, nil)
|
||||
|
||||
entry1, err := monitor.Add("https://foo:12345", rec1.OnLookup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer monitor.Remove(entry1)
|
||||
|
||||
rec1.WaitForExpected(ctx)
|
||||
|
||||
ips2 := []net.IP{
|
||||
ip1,
|
||||
ip2,
|
||||
ip3,
|
||||
}
|
||||
add2 := []net.IP{ip3}
|
||||
keep2 := []net.IP{ip1, ip2}
|
||||
rec1.Expect(ips2, add2, keep2, nil)
|
||||
lookup.Set("foo", ips2)
|
||||
rec1.WaitForExpected(ctx)
|
||||
|
||||
ips3 := []net.IP{
|
||||
ip2,
|
||||
ip3,
|
||||
}
|
||||
keep3 := []net.IP{ip2, ip3}
|
||||
remove3 := []net.IP{ip1}
|
||||
rec1.Expect(ips3, nil, keep3, remove3)
|
||||
lookup.Set("foo", ips3)
|
||||
rec1.WaitForExpected(ctx)
|
||||
|
||||
rec1.ExpectNone()
|
||||
time.Sleep(5 * interval)
|
||||
|
||||
remove4 := []net.IP{ip2, ip3}
|
||||
rec1.Expect(nil, nil, nil, remove4)
|
||||
lookup.Set("foo", nil)
|
||||
rec1.WaitForExpected(ctx)
|
||||
|
||||
rec1.ExpectNone()
|
||||
time.Sleep(5 * interval)
|
||||
|
||||
// Removing multiple times is supported.
|
||||
monitor.Remove(entry1)
|
||||
monitor.Remove(entry1)
|
||||
|
||||
// No more events after removing.
|
||||
lookup.Set("foo", ips1)
|
||||
rec1.ExpectNone()
|
||||
time.Sleep(5 * interval)
|
||||
}
|
||||
|
||||
func TestDnsMonitorIP(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
|
||||
ip := "192.168.0.1"
|
||||
ips := []net.IP{
|
||||
net.ParseIP(ip),
|
||||
}
|
||||
|
||||
rec1 := newDnsMonitorReceiverForTest(t)
|
||||
rec1.Expect(ips, ips, nil, nil)
|
||||
|
||||
entry, err := monitor.Add(ip+":12345", rec1.OnLookup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer monitor.Remove(entry)
|
||||
|
||||
rec1.WaitForExpected(ctx)
|
||||
|
||||
rec1.ExpectNone()
|
||||
time.Sleep(5 * interval)
|
||||
}
|
||||
|
||||
func TestDnsMonitorNoLookupIfEmpty(t *testing.T) {
|
||||
interval := time.Millisecond
|
||||
monitor := newDnsMonitorForTest(t, interval)
|
||||
|
||||
var checked atomic.Bool
|
||||
monitor.checkHostnames = func() {
|
||||
checked.Store(true)
|
||||
monitor.doCheckHostnames()
|
||||
}
|
||||
|
||||
time.Sleep(10 * interval)
|
||||
if checked.Load() {
|
||||
t.Error("should not have checked hostnames")
|
||||
}
|
||||
}
|
||||
|
||||
type deadlockMonitorReceiver struct {
|
||||
t *testing.T
|
||||
monitor *DnsMonitor
|
||||
|
||||
mu sync.RWMutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
entry *DnsMonitorEntry
|
||||
started chan struct{}
|
||||
triggered bool
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func newDeadlockMonitorReceiver(t *testing.T, monitor *DnsMonitor) *deadlockMonitorReceiver {
|
||||
return &deadlockMonitorReceiver{
|
||||
t: t,
|
||||
monitor: monitor,
|
||||
started: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *deadlockMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) {
|
||||
if r.closed.Load() {
|
||||
r.t.Error("received lookup after closed")
|
||||
return
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.triggered {
|
||||
return
|
||||
}
|
||||
|
||||
r.triggered = true
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
close(r.started)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *deadlockMonitorReceiver) Start() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
entry, err := r.monitor.Add("foo", r.OnLookup)
|
||||
if err != nil {
|
||||
r.t.Errorf("error adding listener: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.entry = entry
|
||||
}
|
||||
|
||||
func (r *deadlockMonitorReceiver) Close() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.entry != nil {
|
||||
r.monitor.Remove(r.entry)
|
||||
r.closed.Store(true)
|
||||
}
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func TestDnsMonitorDeadlock(t *testing.T) {
|
||||
lookup := newMockDnsLookupForTest(t)
|
||||
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)
|
||||
|
||||
r := newDeadlockMonitorReceiver(t, monitor)
|
||||
r.Start()
|
||||
<-r.started
|
||||
lookup.Set("foo", []net.IP{ip2})
|
||||
r.Close()
|
||||
lookup.Set("foo", []net.IP{ip1})
|
||||
time.Sleep(10 * interval)
|
||||
monitor.mu.Lock()
|
||||
defer monitor.mu.Unlock()
|
||||
if len(monitor.hostnames) > 0 {
|
||||
t.Errorf("should have cleared hostnames, got %+v", monitor.hostnames)
|
||||
}
|
||||
}
|
134
docker/README.md
Normal file
134
docker/README.md
Normal file
|
@ -0,0 +1,134 @@
|
|||
# Docker images for nextcloud-spreed-signaling
|
||||
|
||||
## Signaling server
|
||||
|
||||
The image for the signaling server can be retrieved from
|
||||
|
||||
strukturag/nextcloud-spreed-signaling:<version>
|
||||
|
||||
Replace `version` with the tag or commit you want to use.
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
The running container can be configured through different environment variables:
|
||||
|
||||
- `CONFIG`: Optional name of configuration file to use.
|
||||
- `HTTP_LISTEN`: Address of HTTP listener.
|
||||
- `HTTPS_LISTEN`: Address of HTTPS listener.
|
||||
- `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).
|
||||
- `BLOCK_KEY`: Key for encrypting data in the sessions (16, 24 or 32 bytes).
|
||||
- `INTERNAL_SHARED_SECRET_KEY`: Shared secret for connections from internal clients.
|
||||
- `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).
|
||||
- `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).
|
||||
- `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).
|
||||
- `ETCD_DISCOVERY_SERVICE`: Optional service name for DNS SRV configuration of etcd..
|
||||
- `ETCD_CLIENT_CERTIFICATE`: Filename of certificate for etcd client.
|
||||
- `ETCD_CLIENT_KEY`: Filename of private key for etcd client.
|
||||
- `ETCD_CLIENT_CA`: Filename of CA for etcd client.
|
||||
- `USE_JANUS`: Set to `1` if Janus should be used as WebRTC backend.
|
||||
- `JANUS_URL`: Url to Janus server (if `USE_JANUS` is set to `1`).
|
||||
- `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_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.
|
||||
- `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.
|
||||
- `GEOIP_LICENSE`: License key to use when downloading the MaxMind GeoIP database.
|
||||
- `GEOIP_URL`: Optional URL to download a MaxMind GeoIP database from.
|
||||
- `GEOIP_OVERRIDES`: Optional space-separated list of overrides for GeoIP lookups.
|
||||
- `CONTINENT_OVERRIDES`: Optional space-separated list of overrides for continent mappings.
|
||||
- `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.
|
||||
- `GRPC_LISTEN`: IP and port to listen on for GRPC requests.
|
||||
- `GRPC_SERVER_CERTIFICATE`: Certificate to use for the GRPC server.
|
||||
- `GRPC_SERVER_KEY`: Private key to use for the GRPC server.
|
||||
- `GRPC_SERVER_CA`: CA certificate that is allowed to issue certificates of GRPC servers.
|
||||
- `GRPC_CLIENT_CERTIFICATE`: Certificate to use for the GRPC client.
|
||||
- `GRPC_CLIENT_KEY`: Private key to use for the GRPC client.
|
||||
- `GRPC_CLIENT_CA`: CA certificate that is allowed to issue certificates of GRPC clients.
|
||||
- `GRPC_TARGETS`: Comma-separated list of GRPC targets to connect to for clustering mode.
|
||||
- `GRPC_DNS_DISCOVERY`: Enable DNS discovery on hostnames of configured GRPC targets.
|
||||
- `GRPC_ETCD`: Set to `1` if etcd should be used to configure GRPC peers.
|
||||
- `GRPC_TARGET_PREFIX`: Key prefix of GRPC target entries.
|
||||
- `SKIP_VERIFY`: Set to `true` to skip certificate validation of backends and proxy servers. This should only be enabled during development, e.g. to work with self-signed certificates.
|
||||
|
||||
Example with two backends:
|
||||
|
||||
docker run \
|
||||
... \
|
||||
-e BACKENDS="foo bar" \
|
||||
-e BACKEND_FOO_URL=https://cloud.server1.tld \
|
||||
-e BACKEND_FOO_SHARED_SECRET=verysecret \
|
||||
-e BACKEND_BAR_URL=https://cloud.server2.tld \
|
||||
-e BACKEND_BAR_SHARED_SECRET=moresecret \
|
||||
...
|
||||
|
||||
See https://github.com/strukturag/nextcloud-spreed-signaling/blob/master/server.conf.in
|
||||
for further details on the different options.
|
||||
|
||||
|
||||
## Signaling proxy
|
||||
|
||||
The image for the signaling proxy can be retrieved from
|
||||
|
||||
strukturag/nextcloud-spreed-signaling:<version>-proxy
|
||||
|
||||
Replace `version` with the tag or commit you want to use.
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
The running container can be configured through different environment variables:
|
||||
|
||||
- `CONFIG`: Optional name of configuration file to use.
|
||||
- `HTTP_LISTEN`: Address of HTTP listener.
|
||||
- `COUNTRY`: Optional ISO 3166 country this proxy is located at.
|
||||
- `EXTERNAL_HOSTNAME`: The external hostname for remote streams. Will try to autodetect if omitted.
|
||||
- `TOKEN_ID`: Id of the token to use when connecting remote streams.
|
||||
- `TOKEN_KEY`: Private key for the configured token id.
|
||||
- `BANDWIDTH_INCOMING`: Optional incoming target bandwidth (in megabits per second).
|
||||
- `BANDWIDTH_OUTGOING`: Optional outgoing target bandwidth (in megabits per second).
|
||||
- `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.
|
||||
- `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).
|
||||
- `ETCD_DISCOVERY_SRV`: Alternative domain to use for DNS SRV configuration of etcd endpoints (if etcd should be used).
|
||||
- `ETCD_DISCOVERY_SERVICE`: Optional service name for DNS SRV configuration of etcd..
|
||||
- `ETCD_CLIENT_CERTIFICATE`: Filename of certificate for etcd client.
|
||||
- `ETCD_CLIENT_KEY`: Filename of private key for etcd client.
|
||||
- `ETCD_CLIENT_CA`: Filename of CA for etcd client.
|
||||
- `TOKENS_ETCD`: Set to `1` if etcd should be used to configure tokens.
|
||||
- `TOKEN_KEY_FORMAT`: Format of key name to retrieve the public key from, "%s" will be replaced with the token id.
|
||||
- `TOKENS`: Space-separated list of token ids.
|
||||
- `TOKEN_<ID>_KEY`: Filename of public key for token `ID` (where `ID` is the uppercase token id).
|
||||
|
||||
Example with two tokens:
|
||||
|
||||
docker run \
|
||||
... \
|
||||
-e TOKENS="foo signaling.server1.tld" \
|
||||
-e TOKEN_FOO_KEY=/path/to/foo.key \
|
||||
-e TOKEN_SIGNALING_SERVER1_TLD_KEY=/path/to/signaling.server1.tld.key \
|
||||
...
|
||||
|
||||
See https://github.com/strukturag/nextcloud-spreed-signaling/blob/master/proxy.conf.in
|
||||
for further details on the different options.
|
|
@ -2,7 +2,11 @@ version: '3'
|
|||
|
||||
services:
|
||||
spreedbackend:
|
||||
build: .
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/server/Dockerfile
|
||||
platforms:
|
||||
- "linux/amd64"
|
||||
volumes:
|
||||
- ./server.conf:/config/server.conf
|
||||
network_mode: host
|
||||
|
@ -19,7 +23,7 @@ services:
|
|||
network_mode: host
|
||||
restart: unless-stopped
|
||||
janus:
|
||||
build: docker/janus
|
||||
build: janus
|
||||
command: ["janus", "--full-trickle"]
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
|
@ -1,5 +1,5 @@
|
|||
# Modified from https://gitlab.com/powerpaul17/nc_talk_backend/-/blob/dcbb918d8716dad1eb72a889d1e6aa1e3a543641/docker/janus/Dockerfile
|
||||
FROM alpine:3.14
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache curl autoconf automake libtool pkgconf build-base \
|
||||
glib-dev libconfig-dev libnice-dev jansson-dev openssl-dev zlib libsrtp-dev \
|
||||
|
@ -15,30 +15,30 @@ RUN cd /tmp && \
|
|||
git checkout $USRSCTP_VERSION && \
|
||||
./bootstrap && \
|
||||
./configure --prefix=/usr && \
|
||||
make && make install
|
||||
make -j$(nproc) && make install
|
||||
|
||||
# libsrtp
|
||||
ARG LIBSRTP_VERSION=2.4.2
|
||||
ARG LIBSRTP_VERSION=2.6.0
|
||||
RUN cd /tmp && \
|
||||
wget https://github.com/cisco/libsrtp/archive/v$LIBSRTP_VERSION.tar.gz && \
|
||||
tar xfv v$LIBSRTP_VERSION.tar.gz && \
|
||||
cd libsrtp-$LIBSRTP_VERSION && \
|
||||
./configure --prefix=/usr --enable-openssl && \
|
||||
make shared_library && \
|
||||
make shared_library -j$(nproc) && \
|
||||
make install && \
|
||||
rm -fr /libsrtp-$LIBSRTP_VERSION && \
|
||||
rm -f /v$LIBSRTP_VERSION.tar.gz
|
||||
|
||||
# JANUS
|
||||
|
||||
ARG JANUS_VERSION=0.11.8
|
||||
ARG JANUS_VERSION=1.2.2
|
||||
RUN mkdir -p /usr/src/janus && \
|
||||
cd /usr/src/janus && \
|
||||
curl -L https://github.com/meetecho/janus-gateway/archive/v$JANUS_VERSION.tar.gz | tar -xz && \
|
||||
cd /usr/src/janus/janus-gateway-$JANUS_VERSION && \
|
||||
./autogen.sh && \
|
||||
./configure --disable-rabbitmq --disable-mqtt --disable-boringssl && \
|
||||
make && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
make configs
|
||||
|
||||
|
|
30
docker/proxy/Dockerfile
Normal file
30
docker/proxy/Dockerfile
Normal file
|
@ -0,0 +1,30 @@
|
|||
FROM --platform=${BUILDPLATFORM} golang:1.22-alpine AS builder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
WORKDIR /workdir
|
||||
|
||||
COPY . .
|
||||
RUN touch /.dockerenv && \
|
||||
apk add --no-cache bash git build-base protobuf && \
|
||||
if [ -d "vendor" ]; then GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOPROXY=off make proxy; else \
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} make proxy; fi
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
ENV CONFIG=/config/proxy.conf
|
||||
RUN adduser -D spreedbackend && \
|
||||
apk add --no-cache bash tzdata ca-certificates
|
||||
|
||||
COPY --from=builder /workdir/bin/proxy /usr/bin/nextcloud-spreed-signaling-proxy
|
||||
COPY ./proxy.conf.in /config/proxy.conf.in
|
||||
COPY ./docker/proxy/entrypoint.sh /
|
||||
COPY ./docker/proxy/stop.sh /
|
||||
COPY ./docker/proxy/wait.sh /
|
||||
RUN chown spreedbackend /config
|
||||
RUN /usr/bin/nextcloud-spreed-signaling-proxy -version
|
||||
|
||||
USER spreedbackend
|
||||
|
||||
STOPSIGNAL SIGUSR1
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
135
docker/proxy/entrypoint.sh
Executable file
135
docker/proxy/entrypoint.sh
Executable file
|
@ -0,0 +1,135 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
set -e
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
# Run custom command.
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
if [ -z "$CONFIG" ]; then
|
||||
echo "No configuration filename given in CONFIG environment variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
echo "Preparing signaling proxy configuration in $CONFIG ..."
|
||||
cp /config/proxy.conf.in "$CONFIG"
|
||||
|
||||
if [ -n "$HTTP_LISTEN" ]; then
|
||||
sed -i "s|#listen = 127.0.0.1:9090|listen = $HTTP_LISTEN|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$COUNTRY" ]; then
|
||||
sed -i "s|#country =.*|country = $COUNTRY|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$EXTERNAL_HOSTNAME" ]; then
|
||||
sed -i "s|#hostname =.*|hostname = $EXTERNAL_HOSTNAME|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$TOKEN_ID" ]; then
|
||||
sed -i "s|#token_id =.*|token_id = $TOKEN_ID|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$TOKEN_KEY" ]; then
|
||||
sed -i "s|#token_key =.*|token_key = $TOKEN_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$BANDWIDTH_INCOMING" ]; then
|
||||
sed -i "s|#incoming =.*|incoming = $BANDWIDTH_INCOMING|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$BANDWIDTH_OUTGOING" ]; then
|
||||
sed -i "s|#outgoing =.*|outgoing = $BANDWIDTH_OUTGOING|" "$CONFIG"
|
||||
fi
|
||||
|
||||
HAS_ETCD=
|
||||
if [ -n "$ETCD_ENDPOINTS" ]; then
|
||||
sed -i "s|#endpoints =.*|endpoints = $ETCD_ENDPOINTS|" "$CONFIG"
|
||||
HAS_ETCD=1
|
||||
else
|
||||
if [ -n "$ETCD_DISCOVERY_SRV" ]; then
|
||||
sed -i "s|#discoverysrv =.*|discoverysrv = $ETCD_DISCOVERY_SRV|" "$CONFIG"
|
||||
HAS_ETCD=1
|
||||
fi
|
||||
if [ -n "$ETCD_DISCOVERY_SERVICE" ]; then
|
||||
sed -i "s|#discoveryservice =.*|discoveryservice = $ETCD_DISCOVERY_SERVICE|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$HAS_ETCD" ]; then
|
||||
if [ -n "$ETCD_CLIENT_KEY" ]; then
|
||||
sed -i "s|#clientkey = /path/to/etcd-client.key|clientkey = $ETCD_CLIENT_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$ETCD_CLIENT_CERTIFICATE" ]; then
|
||||
sed -i "s|#clientcert = /path/to/etcd-client.crt|clientcert = $ETCD_CLIENT_CERTIFICATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$ETCD_CLIENT_CA" ]; then
|
||||
sed -i "s|#cacert = /path/to/etcd-ca.crt|cacert = $ETCD_CLIENT_CA|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$JANUS_URL" ]; then
|
||||
sed -i "s|url =.*|url = $JANUS_URL|" "$CONFIG"
|
||||
else
|
||||
sed -i "s|url =.*|#url =|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$MAX_STREAM_BITRATE" ]; then
|
||||
sed -i "s|#maxstreambitrate =.*|maxstreambitrate = $MAX_STREAM_BITRATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$MAX_SCREEN_BITRATE" ]; then
|
||||
sed -i "s|#maxscreenbitrate =.*|maxscreenbitrate = $MAX_SCREEN_BITRATE|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$TOKENS_ETCD" ]; then
|
||||
if [ -z "$HAS_ETCD" ]; then
|
||||
echo "No etcd endpoint configured, can't use etcd for proxy tokens"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "s|tokentype =.*|tokentype = etcd|" "$CONFIG"
|
||||
|
||||
if [ -n "$TOKEN_KEY_FORMAT" ]; then
|
||||
sed -i "s|#keyformat =.*|keyformat = $TOKEN_KEY_FORMAT|" "$CONFIG"
|
||||
fi
|
||||
else
|
||||
sed -i "s|\[tokens\]|#[tokens]|" "$CONFIG"
|
||||
echo >> "$CONFIG"
|
||||
echo "[tokens]" >> "$CONFIG"
|
||||
for token in $TOKENS; do
|
||||
declare var="TOKEN_${token^^}_KEY"
|
||||
var=${var//./_}
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "$token = ${!var}" >> "$CONFIG"
|
||||
fi
|
||||
done
|
||||
echo >> "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$STATS_IPS" ]; then
|
||||
sed -i "s|#allowed_ips =.*|allowed_ips = $STATS_IPS|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$TRUSTED_PROXIES" ]; then
|
||||
sed -i "s|#trustedproxies =.*|trustedproxies = $TRUSTED_PROXIES|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting signaling proxy with $CONFIG ..."
|
||||
exec /usr/bin/nextcloud-spreed-signaling-proxy -config "$CONFIG"
|
26
docker/proxy/stop.sh
Executable file
26
docker/proxy/stop.sh
Executable file
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
set -e
|
||||
|
||||
echo "Schedule signaling proxy to shutdown ..."
|
||||
exec killall -USR1 nextcloud-spreed-signaling-proxy
|
33
docker/proxy/wait.sh
Executable file
33
docker/proxy/wait.sh
Executable file
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
set -e
|
||||
|
||||
echo "Waiting for signaling proxy to shutdown ..."
|
||||
while true
|
||||
do
|
||||
if ! pgrep nextcloud-spreed-signaling-proxy > /dev/null ; then
|
||||
echo "Signaling proxy has stopped"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
30
docker/server/Dockerfile
Normal file
30
docker/server/Dockerfile
Normal file
|
@ -0,0 +1,30 @@
|
|||
FROM --platform=${BUILDPLATFORM} golang:1.22-alpine AS builder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
WORKDIR /workdir
|
||||
|
||||
COPY . .
|
||||
RUN touch /.dockerenv && \
|
||||
apk add --no-cache bash git build-base protobuf && \
|
||||
if [ -d "vendor" ]; then GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOPROXY=off make server; else \
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} make server; fi
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
ENV CONFIG=/config/server.conf
|
||||
RUN adduser -D spreedbackend && \
|
||||
apk add --no-cache bash tzdata ca-certificates
|
||||
|
||||
COPY --from=builder /workdir/bin/signaling /usr/bin/nextcloud-spreed-signaling
|
||||
COPY ./server.conf.in /config/server.conf.in
|
||||
COPY ./docker/server/entrypoint.sh /
|
||||
COPY ./docker/server/stop.sh /
|
||||
COPY ./docker/server/wait.sh /
|
||||
RUN chown spreedbackend /config
|
||||
RUN /usr/bin/nextcloud-spreed-signaling -version
|
||||
|
||||
USER spreedbackend
|
||||
|
||||
STOPSIGNAL SIGUSR1
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
273
docker/server/entrypoint.sh
Executable file
273
docker/server/entrypoint.sh
Executable file
|
@ -0,0 +1,273 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
set -e
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
# Run custom command.
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
if [ -z "$CONFIG" ]; then
|
||||
echo "No configuration filename given in CONFIG environment variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
echo "Preparing signaling server configuration in $CONFIG ..."
|
||||
cp /config/server.conf.in "$CONFIG"
|
||||
|
||||
if [ -n "$HTTP_LISTEN" ]; then
|
||||
sed -i "s|#listen = 127.0.0.1:8080|listen = $HTTP_LISTEN|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$HTTPS_LISTEN" ]; then
|
||||
sed -i "s|#listen = 127.0.0.1:8443|listen = $HTTPS_LISTEN|" "$CONFIG"
|
||||
|
||||
if [ -n "$HTTPS_CERTIFICATE" ]; then
|
||||
sed -i "s|certificate = /etc/nginx/ssl/server.crt|certificate = $HTTPS_CERTIFICATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$HTTPS_KEY" ]; then
|
||||
sed -i "s|key = /etc/nginx/ssl/server.key|key = $HTTPS_KEY|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$HASH_KEY" ]; then
|
||||
sed -i "s|the-secret-for-session-checksums|$HASH_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$BLOCK_KEY" ]; then
|
||||
sed -i "s|-encryption-key-|$BLOCK_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$INTERNAL_SHARED_SECRET_KEY" ]; then
|
||||
sed -i "s|the-shared-secret-for-internal-clients|$INTERNAL_SHARED_SECRET_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$NATS_URL" ]; then
|
||||
sed -i "s|#url = nats://localhost:4222|url = $NATS_URL|" "$CONFIG"
|
||||
else
|
||||
sed -i "s|#url = nats://localhost:4222|url = nats://loopback|" "$CONFIG"
|
||||
fi
|
||||
|
||||
HAS_ETCD=
|
||||
if [ -n "$ETCD_ENDPOINTS" ]; then
|
||||
sed -i "s|#endpoints =.*|endpoints = $ETCD_ENDPOINTS|" "$CONFIG"
|
||||
HAS_ETCD=1
|
||||
else
|
||||
if [ -n "$ETCD_DISCOVERY_SRV" ]; then
|
||||
sed -i "s|#discoverysrv =.*|discoverysrv = $ETCD_DISCOVERY_SRV|" "$CONFIG"
|
||||
HAS_ETCD=1
|
||||
fi
|
||||
if [ -n "$ETCD_DISCOVERY_SERVICE" ]; then
|
||||
sed -i "s|#discoveryservice =.*|discoveryservice = $ETCD_DISCOVERY_SERVICE|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$HAS_ETCD" ]; then
|
||||
if [ -n "$ETCD_CLIENT_KEY" ]; then
|
||||
sed -i "s|#clientkey = /path/to/etcd-client.key|clientkey = $ETCD_CLIENT_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$ETCD_CLIENT_CERTIFICATE" ]; then
|
||||
sed -i "s|#clientcert = /path/to/etcd-client.crt|clientcert = $ETCD_CLIENT_CERTIFICATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$ETCD_CLIENT_CA" ]; then
|
||||
sed -i "s|#cacert = /path/to/etcd-ca.crt|cacert = $ETCD_CLIENT_CA|" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$USE_JANUS" ]; then
|
||||
sed -i "s|#type =$|type = janus|" "$CONFIG"
|
||||
if [ -n "$JANUS_URL" ]; then
|
||||
sed -i "/proxy URLs to connect to/{n;s|#url =$|url = $JANUS_URL|}" "$CONFIG"
|
||||
fi
|
||||
elif [ -n "$USE_PROXY" ]; then
|
||||
sed -i "s|#type =$|type = proxy|" "$CONFIG"
|
||||
if [ -n "$PROXY_TOKEN_ID" ]; then
|
||||
sed -i "s|#token_id =.*|token_id = $PROXY_TOKEN_ID|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$PROXY_TOKEN_KEY" ]; then
|
||||
sed -i "s|#token_key =.*|token_key = $PROXY_TOKEN_KEY|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$PROXY_ETCD" ]; then
|
||||
if [ -z "$HAS_ETCD" ]; then
|
||||
echo "No etcd endpoint configured, can't use etcd for proxy connections"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "s|#urltype = static|urltype = etcd|" "$CONFIG"
|
||||
|
||||
if [ -n "$PROXY_KEY_PREFIX" ]; then
|
||||
sed -i "s|#keyprefix =.*|keyprefix = $PROXY_KEY_PREFIX|" "$CONFIG"
|
||||
fi
|
||||
else
|
||||
if [ -n "$PROXY_URLS" ]; then
|
||||
sed -i "/proxy URLs to connect to/{n;s|#url =$|url = $PROXY_URLS|}" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$PROXY_DNS_DISCOVERY" ]; then
|
||||
sed -i "/or deleted as necessary/{n;s|#dnsdiscovery =.*|dnsdiscovery = true|}" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$MAX_STREAM_BITRATE" ]; then
|
||||
sed -i "s|#maxstreambitrate =.*|maxstreambitrate = $MAX_STREAM_BITRATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$MAX_SCREEN_BITRATE" ]; then
|
||||
sed -i "s|#maxscreenbitrate =.*|maxscreenbitrate = $MAX_SCREEN_BITRATE|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$SKIP_VERIFY" ]; then
|
||||
sed -i "s|#skipverify =.*|skipverify = $SKIP_VERIFY|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$TURN_API_KEY" ]; then
|
||||
sed -i "s|#\?apikey =.*|apikey = $TURN_API_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$TURN_SECRET" ]; then
|
||||
sed -i "/same as on the TURN server/{n;s|#\?secret =.*|secret = $TURN_SECRET|}" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$TURN_SERVERS" ]; then
|
||||
sed -i "s|#servers =.*|servers = $TURN_SERVERS|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$GEOIP_LICENSE" ]; then
|
||||
sed -i "s|#license =.*|license = $GEOIP_LICENSE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GEOIP_URL" ]; then
|
||||
sed -i "/looking up IP addresses/{n;s|#url =$|url = $GEOIP_URL|}" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$STATS_IPS" ]; then
|
||||
sed -i "s|#allowed_ips =.*|allowed_ips = $STATS_IPS|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$TRUSTED_PROXIES" ]; then
|
||||
sed -i "s|#trustedproxies =.*|trustedproxies = $TRUSTED_PROXIES|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$GRPC_LISTEN" ]; then
|
||||
sed -i "s|#listen = 0.0.0.0:9090|listen = $GRPC_LISTEN|" "$CONFIG"
|
||||
|
||||
if [ -n "$GRPC_SERVER_CERTIFICATE" ]; then
|
||||
sed -i "s|#servercertificate =.*|servercertificate = $GRPC_SERVER_CERTIFICATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GRPC_SERVER_KEY" ]; then
|
||||
sed -i "s|#serverkey =.*|serverkey = $GRPC_SERVER_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GRPC_SERVER_CA" ]; then
|
||||
sed -i "s|#serverca =.*|serverca = $GRPC_SERVER_CA|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GRPC_CLIENT_CERTIFICATE" ]; then
|
||||
sed -i "s|#clientcertificate =.*|clientcertificate = $GRPC_CLIENT_CERTIFICATE|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GRPC_CLIENT_KEY" ]; then
|
||||
sed -i "s|#clientkey = /path/to/grpc-client.key|clientkey = $GRPC_CLIENT_KEY|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GRPC_CLIENT_CA" ]; then
|
||||
sed -i "s|#clientca =.*|clientca = $GRPC_CLIENT_CA|" "$CONFIG"
|
||||
fi
|
||||
if [ -n "$GRPC_ETCD" ]; then
|
||||
if [ -z "$HAS_ETCD" ]; then
|
||||
echo "No etcd endpoint configured, can't use etcd for GRPC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "s|#targettype =$|targettype = etcd|" "$CONFIG"
|
||||
|
||||
if [ -n "$GRPC_TARGET_PREFIX" ]; then
|
||||
sed -i "s|#targetprefix =.*|targetprefix = $GRPC_TARGET_PREFIX|" "$CONFIG"
|
||||
fi
|
||||
else
|
||||
if [ -n "$GRPC_TARGETS" ]; then
|
||||
sed -i "s|#targets =.*|targets = $GRPC_TARGETS|" "$CONFIG"
|
||||
|
||||
if [ -n "$GRPC_DNS_DISCOVERY" ]; then
|
||||
sed -i "/# deleted as necessary/{n;s|#dnsdiscovery =.*|dnsdiscovery = true|}" "$CONFIG"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$GEOIP_OVERRIDES" ]; then
|
||||
sed -i "s|\[geoip-overrides\]|#[geoip-overrides]|" "$CONFIG"
|
||||
echo >> "$CONFIG"
|
||||
echo "[geoip-overrides]" >> "$CONFIG"
|
||||
for override in $GEOIP_OVERRIDES; do
|
||||
echo "$override" >> "$CONFIG"
|
||||
done
|
||||
echo >> "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$CONTINENT_OVERRIDES" ]; then
|
||||
sed -i "s|\[continent-overrides\]|#[continent-overrides]|" "$CONFIG"
|
||||
echo >> "$CONFIG"
|
||||
echo "[continent-overrides]" >> "$CONFIG"
|
||||
for override in $CONTINENT_OVERRIDES; do
|
||||
echo "$override" >> "$CONFIG"
|
||||
done
|
||||
echo >> "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$BACKENDS_ALLOWALL" ]; then
|
||||
sed -i "s|allowall = false|allowall = $BACKENDS_ALLOWALL|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$BACKENDS_ALLOWALL_SECRET" ]; then
|
||||
sed -i "s|#secret = the-shared-secret-for-allowall|secret = $BACKENDS_ALLOWALL_SECRET|" "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ -n "$BACKENDS" ]; then
|
||||
BACKENDS_CONFIG=${BACKENDS// /,}
|
||||
sed -i "s|#backends = .*|backends = $BACKENDS_CONFIG|" "$CONFIG"
|
||||
|
||||
echo >> "$CONFIG"
|
||||
for backend in $BACKENDS; do
|
||||
echo "[$backend]" >> "$CONFIG"
|
||||
|
||||
declare var="BACKEND_${backend^^}_URL"
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "url = ${!var}" >> "$CONFIG"
|
||||
fi
|
||||
|
||||
declare var="BACKEND_${backend^^}_SHARED_SECRET"
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "secret = ${!var}" >> "$CONFIG"
|
||||
fi
|
||||
|
||||
declare var="BACKEND_${backend^^}_SESSION_LIMIT"
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "sessionlimit = ${!var}" >> "$CONFIG"
|
||||
fi
|
||||
|
||||
declare var="BACKEND_${backend^^}_MAX_STREAM_BITRATE"
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "maxstreambitrate = ${!var}" >> "$CONFIG"
|
||||
fi
|
||||
|
||||
declare var="BACKEND_${backend^^}_MAX_SCREEN_BITRATE"
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "maxscreenbitrate = ${!var}" >> "$CONFIG"
|
||||
fi
|
||||
echo >> "$CONFIG"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting signaling server with $CONFIG ..."
|
||||
exec /usr/bin/nextcloud-spreed-signaling -config "$CONFIG"
|
26
docker/server/stop.sh
Executable file
26
docker/server/stop.sh
Executable file
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
set -e
|
||||
|
||||
echo "Schedule signaling server to shutdown ..."
|
||||
exec killall -USR1 nextcloud-spreed-signaling
|
33
docker/server/wait.sh
Executable file
33
docker/server/wait.sh
Executable file
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
set -e
|
||||
|
||||
echo "Waiting for signaling server to shutdown ..."
|
||||
while true
|
||||
do
|
||||
if ! pgrep nextcloud-spreed-signaling > /dev/null ; then
|
||||
echo "Signaling server has stopped"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
|
@ -45,3 +45,9 @@ The following metrics are available:
|
|||
| `signaling_mcu_no_backend_available_total` | Counter | 0.4.0 | Total number of publishing requests where no backend was available | `type` |
|
||||
| `signaling_room_sessions` | Gauge | 0.4.0 | The current number of sessions in a room | `backend`, `room`, `clienttype` |
|
||||
| `signaling_server_messages_total` | Counter | 0.4.0 | The total number of signaling messages | `type` |
|
||||
| `signaling_grpc_clients` | Gauge | 1.0.0 | The current number of GRPC clients | |
|
||||
| `signaling_grpc_client_calls_total` | Counter | 1.0.0 | The total number of GRPC client calls | `method` |
|
||||
| `signaling_grpc_server_calls_total` | Counter | 1.0.0 | The total number of GRPC server calls | `method` |
|
||||
| `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` |
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
sphinx==5.0.0
|
||||
sphinx_rtd_theme==1.0.0
|
||||
readthedocs-sphinx-search==0.1.2
|
||||
jinja2<3.1.0
|
||||
jinja2==3.1.4
|
||||
markdown==3.6
|
||||
mkdocs==1.6.0
|
||||
readthedocs-sphinx-search==0.3.2
|
||||
sphinx==7.3.7
|
||||
sphinx_rtd_theme==2.0.0
|
||||
|
|
|
@ -111,6 +111,23 @@ must contain two additional HTTP headers:
|
|||
- Calculated checksum: `3c4a69ff328299803ac2879614b707c807b4758cf19450755c60656cac46e3bc`
|
||||
|
||||
|
||||
## Welcome message
|
||||
|
||||
When a client connects, the server will immediately send a `welcome` message to
|
||||
notify the client about supported features. This is available if the server
|
||||
supports the `welcome` feature id.
|
||||
|
||||
Message format (Server -> Client):
|
||||
|
||||
{
|
||||
"type": "welcome",
|
||||
"welcome": {
|
||||
"features": ["optional", "list, "of", "feature", "ids"],
|
||||
...additional information about the server...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
## Establish connection
|
||||
|
||||
This must be the first request by a newly connected client and is used to
|
||||
|
@ -123,7 +140,8 @@ Message format (Client -> Server):
|
|||
"id": "unique-request-id",
|
||||
"type": "hello",
|
||||
"hello": {
|
||||
"version": "the-protocol-version-must-be-1.0",
|
||||
"version": "the-protocol-version",
|
||||
"features": ["optional", "list, "of", "client", "feature", "ids"],
|
||||
"auth": {
|
||||
"url": "the-url-to-the-auth-backend",
|
||||
"params": {
|
||||
|
@ -142,7 +160,7 @@ Message format (Server -> Client):
|
|||
"sessionid": "the-unique-session-id",
|
||||
"resumeid": "the-unique-resume-id",
|
||||
"userid": "the-user-id-for-known-users",
|
||||
"version": "the-protocol-version-must-be-1.0",
|
||||
"version": "the-protocol-version",
|
||||
"server": {
|
||||
"features": ["optional", "list, "of", "feature", "ids"],
|
||||
...additional information about the server...
|
||||
|
@ -150,13 +168,87 @@ Message format (Server -> Client):
|
|||
}
|
||||
}
|
||||
|
||||
Please note that the `server` entry is deprecated and will be removed in a
|
||||
future version. Clients should use the data from the
|
||||
[`welcome` message](#welcome-message) instead.
|
||||
|
||||
|
||||
### Protocol version "1.0"
|
||||
|
||||
For protocol version `1.0` in the `hello` request, the `params` from the `auth`
|
||||
field are sent to the Nextcloud backend for [validation](#backend-validation).
|
||||
|
||||
|
||||
### Protocol version "2.0"
|
||||
|
||||
For protocol version `2.0` in the `hello` request, the `params` from the `auth`
|
||||
field must contain a `token` entry containing a [JWT](https://jwt.io/).
|
||||
|
||||
The JWT must contain the following fields:
|
||||
- `iss`: URL of the Nextcloud server that issued the token.
|
||||
- `iat`: Timestamp when the token has been issued.
|
||||
- `exp`: Timestamp of the token expiration.
|
||||
- `sub`: User Id (if known).
|
||||
- `userdata`: Optional JSON containing more user data.
|
||||
|
||||
It must be signed with an RSA, ECDSA or Ed25519 key.
|
||||
|
||||
Example token:
|
||||
```
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwczovL25leHRjbG91ZC1tYXN0ZXIubG9jYWwvIiwiaWF0IjoxNjU0ODQyMDgwLCJleHAiOjE2NTQ4NDIzODAsInN1YiI6ImFkbWluIiwidXNlcmRhdGEiOnsiZGlzcGxheW5hbWUiOiJBZG1pbmlzdHJhdG9yIn19.5rV0jh89_0fG2L-BUPtciu1q49PoYkLboj33EOdD0qQeYcvE7_di2r5WXM1WmKUCOGeX3hzn6qldDMrJBNuxvQ
|
||||
```
|
||||
|
||||
Example public key:
|
||||
```
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIoCsNSCXyxK25zvSKRio0uiBzwub
|
||||
ONq3tiGTPZo3p2Ogn6wAhhsuSxbFuUQDWMX7Tsu9fDzVdwpRHPT4y3V9cA==
|
||||
-----END PUBLIC KEY-----
|
||||
```
|
||||
|
||||
Example payload:
|
||||
```
|
||||
{
|
||||
"iss": "https://nextcloud-master.local/",
|
||||
"iat": 1654842080,
|
||||
"exp": 1654842380,
|
||||
"sub": "admin",
|
||||
"userdata": {
|
||||
"displayname": "Administrator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The public key is retrieved from the capabilities of the Nextcloud instance
|
||||
in `config` key `hello-v2-token-key` inside `signaling`.
|
||||
|
||||
```
|
||||
"spreed": {
|
||||
"features": [
|
||||
"audio",
|
||||
"video",
|
||||
"chat-v2",
|
||||
"conversation-v4",
|
||||
...
|
||||
],
|
||||
"config": {
|
||||
…
|
||||
"signaling": {
|
||||
"hello-v2-token-key": "-----BEGIN RSA PUBLIC KEY----- ..."
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
### Backend validation
|
||||
|
||||
The server validates the connection request against the passed auth backend
|
||||
(needs to make sure the passed url / hostname is in a whitelist). It performs
|
||||
a POST request and passes the provided `params` as JSON payload in the body
|
||||
of the request.
|
||||
For `hello` protocol version `1.0`, the server validates the connection request
|
||||
against the passed auth backend (needs to make sure the passed url / hostname
|
||||
is in a whitelist).
|
||||
|
||||
It performs a POST request and passes the provided `params` as JSON payload in
|
||||
the body of the request.
|
||||
|
||||
Message format (Server -> Auth backend):
|
||||
|
||||
|
@ -215,7 +307,8 @@ Message format (Client -> Server):
|
|||
"id": "unique-request-id",
|
||||
"type": "hello",
|
||||
"hello": {
|
||||
"version": "the-protocol-version-must-be-1.0",
|
||||
"version": "the-protocol-version",
|
||||
"features": ["optional", "list, "of", "client", "feature", "ids"],
|
||||
"auth": {
|
||||
"type": "the-client-type",
|
||||
...other attributes depending on the client type...
|
||||
|
@ -273,7 +366,7 @@ Message format (Client -> Server):
|
|||
"id": "unique-request-id",
|
||||
"type": "hello",
|
||||
"hello": {
|
||||
"version": "the-protocol-version-must-be-1.0",
|
||||
"version": "the-protocol-version",
|
||||
"resumeid": "the-resume-id-from-the-original-hello-response"
|
||||
}
|
||||
}
|
||||
|
@ -285,7 +378,7 @@ Message format (Server -> Client):
|
|||
"type": "hello",
|
||||
"hello": {
|
||||
"sessionid": "the-unique-session-id",
|
||||
"version": "the-protocol-version-must-be-1.0"
|
||||
"version": "the-protocol-version"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,6 +460,26 @@ Message format (Server -> Client):
|
|||
the current room or the properties of a room change.
|
||||
|
||||
|
||||
Message format (Server -> Client if already joined before):
|
||||
|
||||
{
|
||||
"id": "unique-request-id-from-request",
|
||||
"type": "error",
|
||||
"error": {
|
||||
"code": "already_joined",
|
||||
"message": "Human readable error message",
|
||||
"details": {
|
||||
"roomid": "the-room-id",
|
||||
"properties": {
|
||||
...additional room properties...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- Sent if a client tried to join a room it is already in.
|
||||
|
||||
|
||||
### Backend validation
|
||||
|
||||
Rooms are managed by the Nextcloud backend, so the signaling server has to
|
||||
|
@ -680,6 +793,74 @@ Message format (Server -> Client, receive message)
|
|||
- The `userid` is omitted if a message was sent by an anonymous user.
|
||||
|
||||
|
||||
## Control messages
|
||||
|
||||
Similar to regular messages between clients which can be sent by any session,
|
||||
messages with type `control` can only be sent if the permission flag `control`
|
||||
is available.
|
||||
|
||||
These messages can be used to perform actions on clients that should only be
|
||||
possible by some users (e.g. moderators).
|
||||
|
||||
Message format (Client -> Server, mute phone):
|
||||
|
||||
{
|
||||
"id": "unique-request-id",
|
||||
"type": "control",
|
||||
"control": {
|
||||
"recipient": {
|
||||
"type": "session",
|
||||
"sessionid": "the-session-id-to-send-to"
|
||||
},
|
||||
"data": {
|
||||
"type": "mute",
|
||||
"audio": "audio-flags"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
The bit-field `audio-flags` supports the following bits:
|
||||
- `1`: mute speaking (i.e. phone can no longer talk)
|
||||
- `2`: mute listening (i.e. phone is on hold and can no longer hear)
|
||||
|
||||
To unmute, a value of `0` must be sent.
|
||||
|
||||
Message format (Client -> Server, hangup phone):
|
||||
|
||||
{
|
||||
"id": "unique-request-id",
|
||||
"type": "control",
|
||||
"control": {
|
||||
"recipient": {
|
||||
"type": "session",
|
||||
"sessionid": "the-session-id-to-send-to"
|
||||
},
|
||||
"data": {
|
||||
"type": "hangup"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message format (Client -> Server, send DTMF):
|
||||
|
||||
{
|
||||
"id": "unique-request-id",
|
||||
"type": "control",
|
||||
"control": {
|
||||
"recipient": {
|
||||
"type": "session",
|
||||
"sessionid": "the-session-id-to-send-to"
|
||||
},
|
||||
"data": {
|
||||
"type": "dtmf",
|
||||
"digit": "the-digit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Supported digits are `0`-`9`, `*` and `#`.
|
||||
|
||||
|
||||
## Transient data
|
||||
|
||||
Transient data can be used to share data in a room that is valid while sessions
|
||||
|
@ -704,14 +885,17 @@ Message format (Client -> Server):
|
|||
"transient": {
|
||||
"type": "set",
|
||||
"key": "sample-key",
|
||||
"value": "any-json-object"
|
||||
"value": "any-json-object",
|
||||
"ttl": "optional-ttl"
|
||||
}
|
||||
}
|
||||
|
||||
- The `key` must be a string.
|
||||
- The `value` can be of any type (i.e. string, number, array, object, etc.).
|
||||
- The `ttl` is the time to live in nanoseconds. The value will be removed after
|
||||
that time (if it is still present).
|
||||
- Requests to set a value that is already present for the key are silently
|
||||
ignored.
|
||||
ignored. Any TTL value will be updated / removed.
|
||||
|
||||
|
||||
Message format (Server -> Client):
|
||||
|
@ -778,6 +962,105 @@ Message format (Server -> Client):
|
|||
}
|
||||
|
||||
|
||||
## Internal clients
|
||||
|
||||
Internal clients can be used by third-party applications to perform tasks that
|
||||
a regular client can not be used. Examples are adding virtual sessions or
|
||||
sending media without a regular client connected. This is used for example by
|
||||
the SIP bridge to publish mixed phone audio and show "virtual" sessions for the
|
||||
individial phone calls.
|
||||
|
||||
See above for details on how to connect as internal client. By default, internal
|
||||
clients have their "inCall" and the "publishing audio" flags set. Virtual
|
||||
sessions have their "inCall" and the "publishing phone" flags set.
|
||||
|
||||
This can be changed by including the client feature flag `internal-incall`
|
||||
which will require the client to set the flags as necessary.
|
||||
|
||||
|
||||
### Add virtual session
|
||||
|
||||
Message format (Client -> Server):
|
||||
|
||||
{
|
||||
"type": "internal",
|
||||
"internal": {
|
||||
"type": "addsession",
|
||||
"addsession": {
|
||||
"sessionid": "the-virtual-sessionid",
|
||||
"roomid": "the-room-id-to-add-the-session",
|
||||
"userid": "optional-user-id",
|
||||
"user": {
|
||||
...additional data of the user...
|
||||
},
|
||||
"flags": "optional-initial-flags",
|
||||
"incall": "optional-initial-incall",
|
||||
"options": {
|
||||
"actorId": "optional-actor-id",
|
||||
"actorType": "optional-actor-type",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Phone sessions will have `type` set to `phone` in the additional user data
|
||||
(which will be included in the `joined` [room event](#room-events)),
|
||||
`callid` will be the id of the phone call and `number` the target of the call.
|
||||
The call id will match the one returned for accepted outgoing calls and the
|
||||
associated session id can be used to hangup a call or send DTMF tones to it.
|
||||
|
||||
|
||||
### Update virtual session
|
||||
|
||||
Message format (Client -> Server):
|
||||
|
||||
{
|
||||
"type": "internal",
|
||||
"internal": {
|
||||
"type": "updatesession",
|
||||
"updatesession": {
|
||||
"sessionid": "the-virtual-sessionid",
|
||||
"roomid": "the-room-id-to-update-the-session",
|
||||
"flags": "optional-updated-flags",
|
||||
"incall": "optional-updated-incall"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### Remove virtual session
|
||||
|
||||
Message format (Client -> Server):
|
||||
|
||||
{
|
||||
"type": "internal",
|
||||
"internal": {
|
||||
"type": "removesession",
|
||||
"removesession": {
|
||||
"sessionid": "the-virtual-sessionid",
|
||||
"roomid": "the-room-id-to-add-the-session",
|
||||
"userid": "optional-user-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### Change inCall flags of internal client
|
||||
|
||||
Message format (Client -> Server):
|
||||
|
||||
{
|
||||
"type": "internal",
|
||||
"internal": {
|
||||
"type": "incall",
|
||||
"incall": {
|
||||
"incall": "the-incall-flags"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Internal signaling server API
|
||||
|
||||
The signaling server provides an internal API that can be called from Nextcloud
|
||||
|
@ -939,3 +1222,109 @@ Message format (Backend -> Server)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### Notify sessions to switch to a different room
|
||||
|
||||
This can be used to let sessions in a room know that they switch to a different
|
||||
room (available if the server returns the `switchto` feature). The session ids
|
||||
sent should be the Talk room session ids.
|
||||
|
||||
Message format (Backend -> Server, no additional details)
|
||||
|
||||
{
|
||||
"type": "switchto"
|
||||
"switchto" {
|
||||
"roomid": "target-room-id",
|
||||
"sessions": [
|
||||
"the-nextcloud-session-id-1",
|
||||
"the-nextcloud-session-id-2",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Message format (Backend -> Server, with additional details)
|
||||
|
||||
{
|
||||
"type": "switchto"
|
||||
"switchto" {
|
||||
"roomid": "target-room-id",
|
||||
"sessions": {
|
||||
"the-nextcloud-session-id-1": {
|
||||
...arbitrary object to sent to clients...
|
||||
},
|
||||
"the-nextcloud-session-id-2": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
The signaling server will sent messages to the sessions mentioned in the
|
||||
received `switchto` event. If a details object was included for a session, it
|
||||
will be forwarded in the client message, otherwise the `details` will be
|
||||
omitted.
|
||||
|
||||
Message format (Server -> Client):
|
||||
|
||||
{
|
||||
"type": "event"
|
||||
"event": {
|
||||
"target": "room",
|
||||
"type": "switchto",
|
||||
"switchto": {
|
||||
"roomid": "target-room-id",
|
||||
"details": {
|
||||
...arbitrary object to sent to clients...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Clients are expected to follow the `switchto` message. If clients don't switch
|
||||
to the target room after some time, they might get disconnected.
|
||||
|
||||
|
||||
### Start dialout from a room
|
||||
|
||||
Use this to start a phone dialout to a new user in a given room.
|
||||
|
||||
Message format (Backend -> Server)
|
||||
|
||||
{
|
||||
"type": "dialout"
|
||||
"dialout" {
|
||||
"number": "e164-target-number",
|
||||
"options": {
|
||||
...arbitrary options that will be sent back to validate...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Please note that this requires a connected internal client that supports
|
||||
dialout (e.g. the SIP bridge).
|
||||
|
||||
Message format (Server -> Backend, request was accepted)
|
||||
|
||||
{
|
||||
"type": "dialout"
|
||||
"dialout" {
|
||||
"callid": "the-unique-call-id"
|
||||
}
|
||||
}
|
||||
|
||||
Message format (Server -> Backend, request could not be processed)
|
||||
|
||||
{
|
||||
"type": "dialout"
|
||||
"dialout" {
|
||||
"error": {
|
||||
"code": "the-internal-message-id",
|
||||
"message": "human-readable-error-message",
|
||||
"details": {
|
||||
...optional additional details...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
A HTTP error status code will be set in this case.
|
||||
|
|
295
etcd_client.go
Normal file
295
etcd_client.go
Normal file
|
@ -0,0 +1,295 @@
|
|||
/**
|
||||
* 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"go.etcd.io/etcd/client/pkg/v3/srv"
|
||||
"go.etcd.io/etcd/client/pkg/v3/transport"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type EtcdClientListener interface {
|
||||
EtcdClientCreated(client *EtcdClient)
|
||||
}
|
||||
|
||||
type EtcdClientWatcher interface {
|
||||
EtcdWatchCreated(client *EtcdClient, key string)
|
||||
EtcdKeyUpdated(client *EtcdClient, key string, value []byte, prevValue []byte)
|
||||
EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte)
|
||||
}
|
||||
|
||||
type EtcdClient struct {
|
||||
compatSection string
|
||||
|
||||
mu sync.Mutex
|
||||
client atomic.Value
|
||||
listeners map[EtcdClientListener]bool
|
||||
}
|
||||
|
||||
func NewEtcdClient(config *goconf.ConfigFile, compatSection string) (*EtcdClient, error) {
|
||||
result := &EtcdClient{
|
||||
compatSection: compatSection,
|
||||
}
|
||||
if err := result.load(config, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *EtcdClient) getConfigStringWithFallback(config *goconf.ConfigFile, option string) string {
|
||||
value, _ := config.GetString("etcd", option)
|
||||
if value == "" && c.compatSection != "" {
|
||||
value, _ = config.GetString(c.compatSection, option)
|
||||
if value != "" {
|
||||
log.Printf("WARNING: Configuring etcd option \"%s\" in section \"%s\" is deprecated, use section \"etcd\" instead", option, c.compatSection)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *EtcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error {
|
||||
var endpoints []string
|
||||
if endpointsString := c.getConfigStringWithFallback(config, "endpoints"); endpointsString != "" {
|
||||
for _, ep := range strings.Split(endpointsString, ",") {
|
||||
ep := strings.TrimSpace(ep)
|
||||
if ep != "" {
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
} else if discoverySrv := c.getConfigStringWithFallback(config, "discoverysrv"); discoverySrv != "" {
|
||||
discoveryService := c.getConfigStringWithFallback(config, "discoveryservice")
|
||||
clients, err := srv.GetClient("etcd-client", discoverySrv, discoveryService)
|
||||
if err != nil {
|
||||
if !ignoreErrors {
|
||||
return fmt.Errorf("Could not discover etcd endpoints for %s: %w", discoverySrv, err)
|
||||
}
|
||||
} else {
|
||||
endpoints = clients.Endpoints
|
||||
}
|
||||
}
|
||||
|
||||
if len(endpoints) == 0 {
|
||||
if !ignoreErrors {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("No etcd endpoints configured, not changing client")
|
||||
} else {
|
||||
cfg := clientv3.Config{
|
||||
Endpoints: endpoints,
|
||||
|
||||
// set timeout per request to fail fast when the target endpoint is unavailable
|
||||
DialTimeout: time.Second,
|
||||
}
|
||||
|
||||
if logLevel, _ := config.GetString("etcd", "loglevel"); logLevel != "" {
|
||||
var l zapcore.Level
|
||||
if err := l.Set(logLevel); err != nil {
|
||||
return fmt.Errorf("Unsupported etcd log level %s: %w", logLevel, err)
|
||||
}
|
||||
|
||||
logConfig := zap.NewProductionConfig()
|
||||
logConfig.Level = zap.NewAtomicLevelAt(l)
|
||||
cfg.LogConfig = &logConfig
|
||||
}
|
||||
|
||||
clientKey := c.getConfigStringWithFallback(config, "clientkey")
|
||||
clientCert := c.getConfigStringWithFallback(config, "clientcert")
|
||||
caCert := c.getConfigStringWithFallback(config, "cacert")
|
||||
if clientKey != "" && clientCert != "" && caCert != "" {
|
||||
tlsInfo := transport.TLSInfo{
|
||||
CertFile: clientCert,
|
||||
KeyFile: clientKey,
|
||||
TrustedCAFile: caCert,
|
||||
}
|
||||
tlsConfig, err := tlsInfo.ClientConfig()
|
||||
if err != nil {
|
||||
if !ignoreErrors {
|
||||
return fmt.Errorf("Could not setup etcd TLS configuration: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Could not setup TLS configuration, will be disabled (%s)", err)
|
||||
} else {
|
||||
cfg.TLS = tlsConfig
|
||||
}
|
||||
}
|
||||
|
||||
client, err := clientv3.New(cfg)
|
||||
if err != nil {
|
||||
if !ignoreErrors {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Could not create new client from etd endpoints %+v: %s", endpoints, err)
|
||||
} else {
|
||||
prev := c.getEtcdClient()
|
||||
if prev != nil {
|
||||
prev.Close()
|
||||
}
|
||||
c.client.Store(client)
|
||||
log.Printf("Using etcd endpoints %+v", endpoints)
|
||||
c.notifyListeners()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EtcdClient) Close() error {
|
||||
client := c.getEtcdClient()
|
||||
if client != nil {
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EtcdClient) IsConfigured() bool {
|
||||
return c.getEtcdClient() != nil
|
||||
}
|
||||
|
||||
func (c *EtcdClient) getEtcdClient() *clientv3.Client {
|
||||
client := c.client.Load()
|
||||
if client == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return client.(*clientv3.Client)
|
||||
}
|
||||
|
||||
func (c *EtcdClient) syncClient(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
return c.getEtcdClient().Sync(ctx)
|
||||
}
|
||||
|
||||
func (c *EtcdClient) notifyListeners() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for listener := range c.listeners {
|
||||
listener.EtcdClientCreated(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EtcdClient) AddListener(listener EtcdClientListener) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.listeners == nil {
|
||||
c.listeners = make(map[EtcdClientListener]bool)
|
||||
}
|
||||
c.listeners[listener] = true
|
||||
if client := c.getEtcdClient(); client != nil {
|
||||
go listener.EtcdClientCreated(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EtcdClient) RemoveListener(listener EtcdClientListener) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
delete(c.listeners, listener)
|
||||
}
|
||||
|
||||
func (c *EtcdClient) WaitForConnection(ctx context.Context) error {
|
||||
backoff, err := NewExponentialBackoff(initialWaitDelay, maxWaitDelay)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.syncClient(ctx); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Printf("Timeout waiting for etcd client to connect to the cluster, retry in %s", backoff.NextWait())
|
||||
} else {
|
||||
log.Printf("Could not sync etcd client with the cluster, retry in %s: %s", backoff.NextWait(), err)
|
||||
}
|
||||
|
||||
backoff.Wait(ctx)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Client synced, using endpoints %+v", c.getEtcdClient().Endpoints())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EtcdClient) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
|
||||
return c.getEtcdClient().Get(ctx, key, opts...)
|
||||
}
|
||||
|
||||
func (c *EtcdClient) Watch(ctx context.Context, key string, nextRevision int64, watcher EtcdClientWatcher, opts ...clientv3.OpOption) (int64, error) {
|
||||
log.Printf("Wait for leader and start watching on %s (rev=%d)", key, nextRevision)
|
||||
opts = append(opts, clientv3.WithRev(nextRevision), clientv3.WithPrevKV())
|
||||
ch := c.getEtcdClient().Watch(clientv3.WithRequireLeader(ctx), key, opts...)
|
||||
log.Printf("Watch created for %s", key)
|
||||
watcher.EtcdWatchCreated(c, key)
|
||||
for response := range ch {
|
||||
if err := response.Err(); err != nil {
|
||||
return nextRevision, err
|
||||
}
|
||||
|
||||
nextRevision = response.Header.Revision + 1
|
||||
for _, ev := range response.Events {
|
||||
switch ev.Type {
|
||||
case clientv3.EventTypePut:
|
||||
var prevValue []byte
|
||||
if ev.PrevKv != nil {
|
||||
prevValue = ev.PrevKv.Value
|
||||
}
|
||||
watcher.EtcdKeyUpdated(c, string(ev.Kv.Key), ev.Kv.Value, prevValue)
|
||||
case clientv3.EventTypeDelete:
|
||||
var prevValue []byte
|
||||
if ev.PrevKv != nil {
|
||||
prevValue = ev.PrevKv.Value
|
||||
}
|
||||
watcher.EtcdKeyDeleted(c, string(ev.Kv.Key), prevValue)
|
||||
default:
|
||||
log.Printf("Unsupported watch event %s %q -> %q", ev.Type, ev.Kv.Key, ev.Kv.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextRevision, nil
|
||||
}
|
340
etcd_client_test.go
Normal file
340
etcd_client_test.go
Normal file
|
@ -0,0 +1,340 @@
|
|||
/**
|
||||
* 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"
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"go.etcd.io/etcd/server/v3/embed"
|
||||
"go.etcd.io/etcd/server/v3/lease"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
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 NewEtcdForTest(t *testing.T) *embed.Etcd {
|
||||
cfg := embed.NewConfig()
|
||||
cfg.Dir = t.TempDir()
|
||||
os.Chmod(cfg.Dir, 0700) // nolint
|
||||
cfg.LogLevel = "warn"
|
||||
|
||||
u, err := url.Parse(etcdListenUrl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Find a free port to bind the server to.
|
||||
var etcd *embed.Etcd
|
||||
for port := 50000; port < 50100; port++ {
|
||||
u.Host = net.JoinHostPort("localhost", strconv.Itoa(port))
|
||||
cfg.ListenClientUrls = []url.URL{*u}
|
||||
cfg.AdvertiseClientUrls = []url.URL{*u}
|
||||
httpListener := u
|
||||
httpListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+1))
|
||||
cfg.ListenClientHttpUrls = []url.URL{*httpListener}
|
||||
peerListener := u
|
||||
peerListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+2))
|
||||
cfg.ListenPeerUrls = []url.URL{*peerListener}
|
||||
cfg.AdvertisePeerUrls = []url.URL{*peerListener}
|
||||
cfg.InitialCluster = "default=" + peerListener.String()
|
||||
cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)))
|
||||
etcd, err = embed.StartEtcd(cfg)
|
||||
if isErrorAddressAlreadyInUse(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
break
|
||||
}
|
||||
if etcd == nil {
|
||||
t.Fatal("could not find free port")
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
etcd.Close()
|
||||
<-etcd.Server.StopNotify()
|
||||
})
|
||||
// Wait for server to be ready.
|
||||
<-etcd.Server.ReadyNotify()
|
||||
|
||||
return etcd
|
||||
}
|
||||
|
||||
func NewEtcdClientForTest(t *testing.T) (*embed.Etcd, *EtcdClient) {
|
||||
etcd := NewEtcdForTest(t)
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String())
|
||||
config.AddOption("etcd", "loglevel", "error")
|
||||
|
||||
client, err := NewEtcdClient(config, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := client.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
return etcd, client
|
||||
}
|
||||
|
||||
func SetEtcdValue(etcd *embed.Etcd, key string, value []byte) {
|
||||
if kv := etcd.Server.KV(); kv != nil {
|
||||
kv.Put([]byte(key), value, lease.NoLease)
|
||||
kv.Commit()
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteEtcdValue(etcd *embed.Etcd, key string) {
|
||||
if kv := etcd.Server.KV(); kv != nil {
|
||||
kv.DeleteRange([]byte(key), nil)
|
||||
kv.Commit()
|
||||
}
|
||||
}
|
||||
|
||||
func Test_EtcdClient_Get(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
etcd, client := NewEtcdClientForTest(t)
|
||||
|
||||
if response, err := client.Get(context.Background(), "foo"); err != nil {
|
||||
t.Error(err)
|
||||
} else if response.Count != 0 {
|
||||
t.Errorf("expected 0 response, got %d", response.Count)
|
||||
}
|
||||
|
||||
SetEtcdValue(etcd, "foo", []byte("bar"))
|
||||
|
||||
if response, err := client.Get(context.Background(), "foo"); err != nil {
|
||||
t.Error(err)
|
||||
} else if response.Count != 1 {
|
||||
t.Errorf("expected 1 responses, got %d", response.Count)
|
||||
} else if string(response.Kvs[0].Key) != "foo" {
|
||||
t.Errorf("expected key \"foo\", got \"%s\"", string(response.Kvs[0].Key))
|
||||
} else if string(response.Kvs[0].Value) != "bar" {
|
||||
t.Errorf("expected value \"bar\", got \"%s\"", string(response.Kvs[0].Value))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_EtcdClient_GetPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
etcd, client := NewEtcdClientForTest(t)
|
||||
|
||||
if response, err := client.Get(context.Background(), "foo"); err != nil {
|
||||
t.Error(err)
|
||||
} else if response.Count != 0 {
|
||||
t.Errorf("expected 0 response, got %d", response.Count)
|
||||
}
|
||||
|
||||
SetEtcdValue(etcd, "foo", []byte("1"))
|
||||
SetEtcdValue(etcd, "foo/lala", []byte("2"))
|
||||
SetEtcdValue(etcd, "lala/foo", []byte("3"))
|
||||
|
||||
if response, err := client.Get(context.Background(), "foo", clientv3.WithPrefix()); err != nil {
|
||||
t.Error(err)
|
||||
} else if response.Count != 2 {
|
||||
t.Errorf("expected 2 responses, got %d", response.Count)
|
||||
} else if string(response.Kvs[0].Key) != "foo" {
|
||||
t.Errorf("expected key \"foo\", got \"%s\"", string(response.Kvs[0].Key))
|
||||
} else if string(response.Kvs[0].Value) != "1" {
|
||||
t.Errorf("expected value \"1\", got \"%s\"", string(response.Kvs[0].Value))
|
||||
} else if string(response.Kvs[1].Key) != "foo/lala" {
|
||||
t.Errorf("expected key \"foo/lala\", got \"%s\"", string(response.Kvs[1].Key))
|
||||
} else if string(response.Kvs[1].Value) != "2" {
|
||||
t.Errorf("expected value \"2\", got \"%s\"", string(response.Kvs[1].Value))
|
||||
}
|
||||
}
|
||||
|
||||
type etcdEvent struct {
|
||||
t mvccpb.Event_EventType
|
||||
key string
|
||||
value string
|
||||
|
||||
prevValue string
|
||||
}
|
||||
|
||||
type EtcdClientTestListener struct {
|
||||
t *testing.T
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
initial chan struct{}
|
||||
events chan etcdEvent
|
||||
}
|
||||
|
||||
func NewEtcdClientTestListener(ctx context.Context, t *testing.T) *EtcdClientTestListener {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &EtcdClientTestListener{
|
||||
t: t,
|
||||
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
initial: make(chan struct{}),
|
||||
events: make(chan etcdEvent),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *EtcdClientTestListener) Close() {
|
||||
l.cancel()
|
||||
}
|
||||
|
||||
func (l *EtcdClientTestListener) EtcdClientCreated(client *EtcdClient) {
|
||||
go func() {
|
||||
if err := client.WaitForConnection(l.ctx); err != nil {
|
||||
l.t.Errorf("error waiting for connection: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(l.ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
response, err := client.Get(ctx, "foo", clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
l.t.Error(err)
|
||||
} else if response.Count != 1 {
|
||||
l.t.Errorf("expected 1 responses, got %d", response.Count)
|
||||
} else if string(response.Kvs[0].Key) != "foo/a" {
|
||||
l.t.Errorf("expected key \"foo/a\", got \"%s\"", string(response.Kvs[0].Key))
|
||||
} else if string(response.Kvs[0].Value) != "1" {
|
||||
l.t.Errorf("expected value \"1\", got \"%s\"", string(response.Kvs[0].Value))
|
||||
}
|
||||
|
||||
close(l.initial)
|
||||
nextRevision := response.Header.Revision + 1
|
||||
for l.ctx.Err() == nil {
|
||||
var err error
|
||||
if nextRevision, err = client.Watch(clientv3.WithRequireLeader(l.ctx), "foo", nextRevision, l, clientv3.WithPrefix()); err != nil {
|
||||
l.t.Error(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (l *EtcdClientTestListener) EtcdWatchCreated(client *EtcdClient, key string) {
|
||||
}
|
||||
|
||||
func (l *EtcdClientTestListener) EtcdKeyUpdated(client *EtcdClient, key string, value []byte, prevValue []byte) {
|
||||
evt := etcdEvent{
|
||||
t: clientv3.EventTypePut,
|
||||
key: string(key),
|
||||
value: string(value),
|
||||
}
|
||||
if len(prevValue) > 0 {
|
||||
evt.prevValue = string(prevValue)
|
||||
}
|
||||
l.events <- evt
|
||||
}
|
||||
|
||||
func (l *EtcdClientTestListener) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) {
|
||||
evt := etcdEvent{
|
||||
t: clientv3.EventTypeDelete,
|
||||
key: string(key),
|
||||
}
|
||||
if len(prevValue) > 0 {
|
||||
evt.prevValue = string(prevValue)
|
||||
}
|
||||
l.events <- evt
|
||||
}
|
||||
|
||||
func Test_EtcdClient_Watch(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
etcd, client := NewEtcdClientForTest(t)
|
||||
|
||||
SetEtcdValue(etcd, "foo/a", []byte("1"))
|
||||
|
||||
listener := NewEtcdClientTestListener(context.Background(), t)
|
||||
defer listener.Close()
|
||||
|
||||
client.AddListener(listener)
|
||||
defer client.RemoveListener(listener)
|
||||
|
||||
<-listener.initial
|
||||
|
||||
SetEtcdValue(etcd, "foo/b", []byte("2"))
|
||||
event := <-listener.events
|
||||
if event.t != clientv3.EventTypePut {
|
||||
t.Errorf("expected type %d, got %d", clientv3.EventTypePut, event.t)
|
||||
} else if event.key != "foo/b" {
|
||||
t.Errorf("expected key %s, got %s", "foo/b", event.key)
|
||||
} else if event.value != "2" {
|
||||
t.Errorf("expected value %s, got %s", "2", event.value)
|
||||
}
|
||||
|
||||
SetEtcdValue(etcd, "foo/a", []byte("3"))
|
||||
event = <-listener.events
|
||||
if event.t != clientv3.EventTypePut {
|
||||
t.Errorf("expected type %d, got %d", clientv3.EventTypePut, event.t)
|
||||
} else if event.key != "foo/a" {
|
||||
t.Errorf("expected key %s, got %s", "foo/a", event.key)
|
||||
} else if event.value != "3" {
|
||||
t.Errorf("expected value %s, got %s", "3", event.value)
|
||||
}
|
||||
|
||||
DeleteEtcdValue(etcd, "foo/a")
|
||||
event = <-listener.events
|
||||
if event.t != clientv3.EventTypeDelete {
|
||||
t.Errorf("expected type %d, got %d", clientv3.EventTypeDelete, event.t)
|
||||
} else if event.key != "foo/a" {
|
||||
t.Errorf("expected key %s, got %s", "foo/a", event.key)
|
||||
} else if event.prevValue != "3" {
|
||||
t.Errorf("expected previous value %s, got %s", "3", event.prevValue)
|
||||
}
|
||||
}
|
168
file_watcher.go
Normal file
168
file_watcher.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* 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 signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDeduplicateWatchEvents = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
deduplicateWatchEvents atomic.Int64
|
||||
)
|
||||
|
||||
func init() {
|
||||
deduplicateWatchEvents.Store(int64(defaultDeduplicateWatchEvents))
|
||||
}
|
||||
|
||||
type FileWatcherCallback func(filename string)
|
||||
|
||||
type FileWatcher struct {
|
||||
filename string
|
||||
target string
|
||||
callback FileWatcherCallback
|
||||
|
||||
watcher *fsnotify.Watcher
|
||||
closeCtx context.Context
|
||||
closeFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewFileWatcher(filename string, callback FileWatcherCallback) (*FileWatcher, error) {
|
||||
realFilename, err := filepath.EvalSymlinks(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := watcher.Add(realFilename); err != nil {
|
||||
watcher.Close() // nolint
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := watcher.Add(path.Dir(filename)); err != nil {
|
||||
watcher.Close() // nolint
|
||||
return nil, err
|
||||
}
|
||||
|
||||
closeCtx, closeFunc := context.WithCancel(context.Background())
|
||||
|
||||
w := &FileWatcher{
|
||||
filename: filename,
|
||||
target: realFilename,
|
||||
callback: callback,
|
||||
watcher: watcher,
|
||||
|
||||
closeCtx: closeCtx,
|
||||
closeFunc: closeFunc,
|
||||
}
|
||||
go w.run()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (f *FileWatcher) Close() error {
|
||||
f.closeFunc()
|
||||
return f.watcher.Close()
|
||||
}
|
||||
|
||||
func (f *FileWatcher) run() {
|
||||
var mu sync.Mutex
|
||||
timers := make(map[string]*time.Timer)
|
||||
|
||||
triggerEvent := func(event fsnotify.Event) {
|
||||
deduplicate := time.Duration(deduplicateWatchEvents.Load())
|
||||
if deduplicate <= 0 {
|
||||
f.callback(f.filename)
|
||||
return
|
||||
}
|
||||
|
||||
// Use timer to deduplicate multiple events for the same file.
|
||||
mu.Lock()
|
||||
t, found := timers[event.Name]
|
||||
mu.Unlock()
|
||||
if !found {
|
||||
t = time.AfterFunc(deduplicate, func() {
|
||||
f.callback(f.filename)
|
||||
|
||||
mu.Lock()
|
||||
delete(timers, event.Name)
|
||||
mu.Unlock()
|
||||
})
|
||||
mu.Lock()
|
||||
timers[event.Name] = t
|
||||
mu.Unlock()
|
||||
} else {
|
||||
t.Reset(deduplicate)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-f.watcher.Events:
|
||||
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) && !event.Has(fsnotify.Rename) {
|
||||
continue
|
||||
}
|
||||
|
||||
if stat, err := os.Lstat(event.Name); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("Could not lstat %s: %s", event.Name, err)
|
||||
}
|
||||
} else if stat.Mode()&os.ModeSymlink != 0 {
|
||||
target, err := filepath.EvalSymlinks(event.Name)
|
||||
if err == nil && target != f.target && strings.HasSuffix(event.Name, f.filename) {
|
||||
f.target = target
|
||||
triggerEvent(event)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(event.Name, f.filename) || strings.HasSuffix(event.Name, f.target) {
|
||||
triggerEvent(event)
|
||||
}
|
||||
case err := <-f.watcher.Errors:
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Error watching %s: %s", f.filename, err)
|
||||
case <-f.closeCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
310
file_watcher_test.go
Normal file
310
file_watcher_test.go
Normal file
|
@ -0,0 +1,310 @@
|
|||
/**
|
||||
* 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 signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
testWatcherNoEventTimeout = 2 * defaultDeduplicateWatchEvents
|
||||
)
|
||||
|
||||
func TestFileWatcher_NotExist(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
w, err := NewFileWatcher(path.Join(tmpdir, "test.txt"), func(filename string) {})
|
||||
if err == nil {
|
||||
t.Error("should not be able to watch non-existing files")
|
||||
if err := w.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWatcher_File(t *testing.T) {
|
||||
ensureNoGoroutinesLeak(t, func(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
filename := path.Join(tmpdir, "test.txt")
|
||||
if err := os.WriteFile(filename, []byte("Hello world!"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modified := make(chan struct{})
|
||||
w, err := NewFileWatcher(filename, func(filename string) {
|
||||
modified <- struct{}{}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
if err := os.WriteFile(filename, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-modified
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filename, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-modified
|
||||
|
||||
ctxTimeout, cancel = context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileWatcher_Rename(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
filename := path.Join(tmpdir, "test.txt")
|
||||
if err := os.WriteFile(filename, []byte("Hello world!"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modified := make(chan struct{})
|
||||
w, err := NewFileWatcher(filename, func(filename string) {
|
||||
modified <- struct{}{}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
filename2 := path.Join(tmpdir, "test.txt.tmp")
|
||||
if err := os.WriteFile(filename2, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
|
||||
if err := os.Rename(filename2, filename); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-modified
|
||||
|
||||
ctxTimeout, cancel = context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWatcher_Symlink(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
sourceFilename := path.Join(tmpdir, "test1.txt")
|
||||
if err := os.WriteFile(sourceFilename, []byte("Hello world!"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filename := path.Join(tmpdir, "symlink.txt")
|
||||
if err := os.Symlink(sourceFilename, filename); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modified := make(chan struct{})
|
||||
w, err := NewFileWatcher(filename, func(filename string) {
|
||||
modified <- struct{}{}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
if err := os.WriteFile(sourceFilename, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-modified
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWatcher_ChangeSymlinkTarget(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
sourceFilename1 := path.Join(tmpdir, "test1.txt")
|
||||
if err := os.WriteFile(sourceFilename1, []byte("Hello world!"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sourceFilename2 := path.Join(tmpdir, "test2.txt")
|
||||
if err := os.WriteFile(sourceFilename2, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filename := path.Join(tmpdir, "symlink.txt")
|
||||
if err := os.Symlink(sourceFilename1, filename); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modified := make(chan struct{})
|
||||
w, err := NewFileWatcher(filename, func(filename string) {
|
||||
modified <- struct{}{}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
// Replace symlink by creating new one and rename it to the original target.
|
||||
if err := os.Symlink(sourceFilename2, filename+".tmp"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Rename(filename+".tmp", filename); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-modified
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWatcher_OtherSymlink(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
sourceFilename1 := path.Join(tmpdir, "test1.txt")
|
||||
if err := os.WriteFile(sourceFilename1, []byte("Hello world!"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sourceFilename2 := path.Join(tmpdir, "test2.txt")
|
||||
if err := os.WriteFile(sourceFilename2, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filename := path.Join(tmpdir, "symlink.txt")
|
||||
if err := os.Symlink(sourceFilename1, filename); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modified := make(chan struct{})
|
||||
w, err := NewFileWatcher(filename, func(filename string) {
|
||||
modified <- struct{}{}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
if err := os.Symlink(sourceFilename2, filename+".tmp"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received event for other symlink")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWatcher_RenameSymlinkTarget(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
sourceFilename1 := path.Join(tmpdir, "test1.txt")
|
||||
if err := os.WriteFile(sourceFilename1, []byte("Hello world!"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filename := path.Join(tmpdir, "test.txt")
|
||||
if err := os.Symlink(sourceFilename1, filename); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modified := make(chan struct{})
|
||||
w, err := NewFileWatcher(filename, func(filename string) {
|
||||
modified <- struct{}{}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
sourceFilename2 := path.Join(tmpdir, "test1.txt.tmp")
|
||||
if err := os.WriteFile(sourceFilename2, []byte("Updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
|
||||
if err := os.Rename(sourceFilename2, sourceFilename1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-modified
|
||||
|
||||
ctxTimeout, cancel = context.WithTimeout(context.Background(), testWatcherNoEventTimeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-modified:
|
||||
t.Error("should not have received another event")
|
||||
case <-ctxTimeout.Done():
|
||||
}
|
||||
}
|
77
flags.go
Normal file
77
flags.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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/atomic"
|
||||
)
|
||||
|
||||
type Flags struct {
|
||||
flags atomic.Uint32
|
||||
}
|
||||
|
||||
func (f *Flags) Add(flags uint32) bool {
|
||||
for {
|
||||
old := f.flags.Load()
|
||||
if old&flags == flags {
|
||||
// Flags already set.
|
||||
return false
|
||||
}
|
||||
newFlags := old | flags
|
||||
if f.flags.CompareAndSwap(old, newFlags) {
|
||||
return true
|
||||
}
|
||||
// Another thread updated the flags while we were checking, retry.
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flags) Remove(flags uint32) bool {
|
||||
for {
|
||||
old := f.flags.Load()
|
||||
if old&flags == 0 {
|
||||
// Flags not set.
|
||||
return false
|
||||
}
|
||||
newFlags := old & ^flags
|
||||
if f.flags.CompareAndSwap(old, newFlags) {
|
||||
return true
|
||||
}
|
||||
// Another thread updated the flags while we were checking, retry.
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flags) Set(flags uint32) bool {
|
||||
for {
|
||||
old := f.flags.Load()
|
||||
if old == flags {
|
||||
return false
|
||||
}
|
||||
|
||||
if f.flags.CompareAndSwap(old, flags) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flags) Get() uint32 {
|
||||
return f.flags.Load()
|
||||
}
|
143
flags_test.go
Normal file
143
flags_test.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2023 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"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
var f Flags
|
||||
if f.Get() != 0 {
|
||||
t.Fatalf("Expected flags 0, got %d", f.Get())
|
||||
}
|
||||
if !f.Add(1) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if f.Get() != 1 {
|
||||
t.Fatalf("Expected flags 1, got %d", f.Get())
|
||||
}
|
||||
if f.Add(1) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
if f.Get() != 1 {
|
||||
t.Fatalf("Expected flags 1, got %d", f.Get())
|
||||
}
|
||||
if !f.Add(2) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if f.Get() != 3 {
|
||||
t.Fatalf("Expected flags 3, got %d", f.Get())
|
||||
}
|
||||
if !f.Remove(1) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if f.Get() != 2 {
|
||||
t.Fatalf("Expected flags 2, got %d", f.Get())
|
||||
}
|
||||
if f.Remove(1) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
if f.Get() != 2 {
|
||||
t.Fatalf("Expected flags 2, got %d", f.Get())
|
||||
}
|
||||
if !f.Add(3) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if f.Get() != 3 {
|
||||
t.Fatalf("Expected flags 3, got %d", f.Get())
|
||||
}
|
||||
if !f.Remove(1) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if f.Get() != 2 {
|
||||
t.Fatalf("Expected flags 2, got %d", f.Get())
|
||||
}
|
||||
}
|
||||
|
||||
func runConcurrentFlags(t *testing.T, count int, f func()) {
|
||||
var start sync.WaitGroup
|
||||
start.Add(1)
|
||||
var ready sync.WaitGroup
|
||||
var done sync.WaitGroup
|
||||
for i := 0; i < count; i++ {
|
||||
done.Add(1)
|
||||
ready.Add(1)
|
||||
go func() {
|
||||
defer done.Done()
|
||||
ready.Done()
|
||||
start.Wait()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
ready.Wait()
|
||||
start.Done()
|
||||
done.Wait()
|
||||
}
|
||||
|
||||
func TestFlagsConcurrentAdd(t *testing.T) {
|
||||
t.Parallel()
|
||||
var flags Flags
|
||||
|
||||
var added atomic.Int32
|
||||
runConcurrentFlags(t, 100, func() {
|
||||
if flags.Add(1) {
|
||||
added.Add(1)
|
||||
}
|
||||
})
|
||||
if added.Load() != 1 {
|
||||
t.Errorf("expected only one successfull attempt, got %d", added.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagsConcurrentRemove(t *testing.T) {
|
||||
t.Parallel()
|
||||
var flags Flags
|
||||
flags.Set(1)
|
||||
|
||||
var removed atomic.Int32
|
||||
runConcurrentFlags(t, 100, func() {
|
||||
if flags.Remove(1) {
|
||||
removed.Add(1)
|
||||
}
|
||||
})
|
||||
if removed.Load() != 1 {
|
||||
t.Errorf("expected only one successfull attempt, got %d", removed.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagsConcurrentSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
var flags Flags
|
||||
|
||||
var set atomic.Int32
|
||||
runConcurrentFlags(t, 100, func() {
|
||||
if flags.Set(1) {
|
||||
set.Add(1)
|
||||
}
|
||||
})
|
||||
if set.Load() != 1 {
|
||||
t.Errorf("expected only one successfull attempt, got %d", set.Load())
|
||||
}
|
||||
}
|
100
geoip.go
100
geoip.go
|
@ -35,6 +35,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
|
@ -156,36 +157,45 @@ func (g *GeoLookup) updateUrl() error {
|
|||
}
|
||||
|
||||
body := response.Body
|
||||
if strings.HasSuffix(g.url, ".gz") {
|
||||
url := g.url
|
||||
if strings.HasSuffix(url, ".gz") {
|
||||
body, err = gzip.NewReader(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url = strings.TrimSuffix(url, ".gz")
|
||||
}
|
||||
|
||||
tarfile := tar.NewReader(body)
|
||||
var geoipdata []byte
|
||||
for {
|
||||
header, err := tarfile.Next()
|
||||
if err == io.EOF {
|
||||
if strings.HasSuffix(url, ".tar") || strings.HasSuffix(url, "=tar") {
|
||||
tarfile := tar.NewReader(body)
|
||||
for {
|
||||
header, err := tarfile.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(header.Name, ".mmdb") {
|
||||
continue
|
||||
}
|
||||
|
||||
geoipdata, err = io.ReadAll(tarfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(header.Name, ".mmdb") {
|
||||
continue
|
||||
}
|
||||
|
||||
geoipdata, err = io.ReadAll(tarfile)
|
||||
} else {
|
||||
geoipdata, err = io.ReadAll(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if len(geoipdata) == 0 {
|
||||
return fmt.Errorf("did not find MaxMind database in tarball from %s", g.url)
|
||||
return fmt.Errorf("did not find GeoIP database in download from %s", g.url)
|
||||
}
|
||||
|
||||
reader, err := maxminddb.FromBytes(geoipdata)
|
||||
|
@ -267,3 +277,63 @@ func IsValidContinent(continent string) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func LoadGeoIPOverrides(config *goconf.ConfigFile, ignoreErrors bool) (map[*net.IPNet]string, error) {
|
||||
options, _ := GetStringOptions(config, "geoip-overrides", true)
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
geoipOverrides := make(map[*net.IPNet]string, len(options))
|
||||
for option, value := range options {
|
||||
var ip net.IP
|
||||
var ipNet *net.IPNet
|
||||
if strings.Contains(option, "/") {
|
||||
_, ipNet, err = net.ParseCIDR(option)
|
||||
if err != nil {
|
||||
if ignoreErrors {
|
||||
log.Printf("could not parse CIDR %s (%s), skipping", option, err)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not parse CIDR %s: %s", option, err)
|
||||
}
|
||||
} else {
|
||||
ip = net.ParseIP(option)
|
||||
if ip == nil {
|
||||
if ignoreErrors {
|
||||
log.Printf("could not parse IP %s, skipping", option)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not parse IP %s", option)
|
||||
}
|
||||
|
||||
var mask net.IPMask
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
mask = net.CIDRMask(32, 32)
|
||||
} else {
|
||||
mask = net.CIDRMask(128, 128)
|
||||
}
|
||||
ipNet = &net.IPNet{
|
||||
IP: ip,
|
||||
Mask: mask,
|
||||
}
|
||||
}
|
||||
|
||||
value = strings.ToUpper(strings.TrimSpace(value))
|
||||
if value == "" {
|
||||
log.Printf("IP %s doesn't have a country assigned, skipping", option)
|
||||
continue
|
||||
} else if !IsValidCountry(value) {
|
||||
log.Printf("Country %s for IP %s is invalid, skipping", value, option)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Using country %s for %s", value, ipNet)
|
||||
geoipOverrides[ipNet] = value
|
||||
}
|
||||
|
||||
return geoipOverrides, nil
|
||||
}
|
||||
|
|
|
@ -24,12 +24,14 @@ package signaling
|
|||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testGeoLookupReader(t *testing.T, reader *GeoLookup) {
|
||||
|
@ -57,13 +59,27 @@ func testGeoLookupReader(t *testing.T, reader *GeoLookup) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGeoLookup(t *testing.T) {
|
||||
license := os.Getenv("MAXMIND_GEOLITE2_LICENSE")
|
||||
if license == "" {
|
||||
t.Skip("No MaxMind GeoLite2 license was set in MAXMIND_GEOLITE2_LICENSE environment variable.")
|
||||
}
|
||||
func GetGeoIpUrlForTest(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
reader, err := NewGeoLookupFromUrl(GetGeoIpDownloadUrl(license))
|
||||
var geoIpUrl string
|
||||
if os.Getenv("USE_DB_IP_GEOIP_DATABASE") != "" {
|
||||
now := time.Now().UTC()
|
||||
geoIpUrl = fmt.Sprintf("https://download.db-ip.com/free/dbip-country-lite-%d-%.2d.mmdb.gz", now.Year(), now.Month())
|
||||
}
|
||||
if geoIpUrl == "" {
|
||||
license := os.Getenv("MAXMIND_GEOLITE2_LICENSE")
|
||||
if license == "" {
|
||||
t.Skip("No MaxMind GeoLite2 license was set in MAXMIND_GEOLITE2_LICENSE environment variable.")
|
||||
}
|
||||
geoIpUrl = GetGeoIpDownloadUrl(license)
|
||||
}
|
||||
return geoIpUrl
|
||||
}
|
||||
|
||||
func TestGeoLookup(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
reader, err := NewGeoLookupFromUrl(GetGeoIpUrlForTest(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -77,12 +93,8 @@ func TestGeoLookup(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGeoLookupCaching(t *testing.T) {
|
||||
license := os.Getenv("MAXMIND_GEOLITE2_LICENSE")
|
||||
if license == "" {
|
||||
t.Skip("No MaxMind GeoLite2 license was set in MAXMIND_GEOLITE2_LICENSE environment variable.")
|
||||
}
|
||||
|
||||
reader, err := NewGeoLookupFromUrl(GetGeoIpDownloadUrl(license))
|
||||
CatchLogForTest(t)
|
||||
reader, err := NewGeoLookupFromUrl(GetGeoIpUrlForTest(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -128,6 +140,7 @@ func TestGeoLookupContinent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGeoLookupCloseEmpty(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
reader, err := NewGeoLookupFromUrl("ignore-url")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -136,24 +149,23 @@ func TestGeoLookupCloseEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGeoLookupFromFile(t *testing.T) {
|
||||
license := os.Getenv("MAXMIND_GEOLITE2_LICENSE")
|
||||
if license == "" {
|
||||
t.Skip("No MaxMind GeoLite2 license was set in MAXMIND_GEOLITE2_LICENSE environment variable.")
|
||||
}
|
||||
CatchLogForTest(t)
|
||||
geoIpUrl := GetGeoIpUrlForTest(t)
|
||||
|
||||
url := GetGeoIpDownloadUrl(license)
|
||||
resp, err := http.Get(url)
|
||||
resp, err := http.Get(geoIpUrl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body := resp.Body
|
||||
if strings.HasSuffix(url, ".gz") {
|
||||
url := geoIpUrl
|
||||
if strings.HasSuffix(geoIpUrl, ".gz") {
|
||||
body, err = gzip.NewReader(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
url = strings.TrimSuffix(url, ".gz")
|
||||
}
|
||||
|
||||
tmpfile, err := os.CreateTemp("", "geoipdb")
|
||||
|
@ -164,21 +176,33 @@ func TestGeoLookupFromFile(t *testing.T) {
|
|||
os.Remove(tmpfile.Name())
|
||||
})
|
||||
|
||||
tarfile := tar.NewReader(body)
|
||||
foundDatabase := false
|
||||
for {
|
||||
header, err := tarfile.Next()
|
||||
if err == io.EOF {
|
||||
if strings.HasSuffix(url, ".tar") || strings.HasSuffix(url, "=tar") {
|
||||
tarfile := tar.NewReader(body)
|
||||
for {
|
||||
header, err := tarfile.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(header.Name, ".mmdb") {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tmpfile, tarfile); err != nil {
|
||||
tmpfile.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foundDatabase = true
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(header.Name, ".mmdb") {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tmpfile, tarfile); err != nil {
|
||||
} else {
|
||||
if _, err := io.Copy(tmpfile, body); err != nil {
|
||||
tmpfile.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -186,11 +210,10 @@ func TestGeoLookupFromFile(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
foundDatabase = true
|
||||
break
|
||||
}
|
||||
|
||||
if !foundDatabase {
|
||||
t.Fatal("Did not find MaxMind database in tarball")
|
||||
t.Fatalf("Did not find GeoIP database in download from %s", geoIpUrl)
|
||||
}
|
||||
|
||||
reader, err := NewGeoLookupFromFile(tmpfile.Name())
|
||||
|
|
108
go.mod
108
go.mod
|
@ -1,85 +1,89 @@
|
|||
module github.com/strukturag/nextcloud-spreed-signaling
|
||||
|
||||
go 1.17
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/nats-io/nats-server/v2 v2.8.4
|
||||
github.com/nats-io/nats.go v1.16.0
|
||||
github.com/nats-io/nats-server/v2 v2.10.16
|
||||
github.com/nats-io/nats.go v1.35.0
|
||||
github.com/notedit/janus-go v0.0.0-20200517101215-10eb8b95d1a0
|
||||
github.com/oschwald/maxminddb-golang v1.9.0
|
||||
github.com/pion/sdp v1.3.0
|
||||
github.com/prometheus/client_golang v1.12.2
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4
|
||||
go.etcd.io/etcd/client/v3 v3.5.4
|
||||
go.etcd.io/etcd/server/v3 v3.5.4
|
||||
github.com/oschwald/maxminddb-golang v1.12.0
|
||||
github.com/pion/sdp/v3 v3.0.9
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
go.etcd.io/etcd/api/v3 v3.5.13
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13
|
||||
go.etcd.io/etcd/client/v3 v3.5.13
|
||||
go.etcd.io/etcd/server/v3 v3.5.13
|
||||
go.uber.org/zap v1.27.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.14.4 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/minio/highwayhash v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a // indirect
|
||||
github.com/nats-io/nkeys v0.3.0 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.5.7 // indirect
|
||||
github.com/nats-io/nkeys v0.4.7 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.4 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.4 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.4 // indirect
|
||||
go.opentelemetry.io/contrib v0.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v0.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.17.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220325203850-36772127a21f // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
|
||||
google.golang.org/grpc v1.38.0 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.13 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect
|
||||
go.opentelemetry.io/otel v1.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
|
|
759
go.sum
759
go.sum
|
@ -1,97 +1,33 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg=
|
||||
github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI=
|
||||
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
|
||||
github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs=
|
||||
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY=
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc=
|
||||
github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
|
||||
github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
|
||||
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490 h1:I8/Qu5NTaiXi1TsEYmTeLDUlf7u9pEdbG+azjDvx8Vg=
|
||||
github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490/go.mod h1:jWlUIP63OLr0cV2FGN2IEzSFsMAe58if8rk/SAE0JRE=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
|
@ -99,686 +35,291 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
|||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
|
||||
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=
|
||||
github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a h1:lem6QCvxR0Y28gth9P+wV2K/zYUUAkJ+55U8cpS0p5I=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
|
||||
github.com/nats-io/nats-server/v2 v2.8.4 h1:0jQzze1T9mECg8YZEl8+WYUXb9JKluJfCBriPUtluB4=
|
||||
github.com/nats-io/nats-server/v2 v2.8.4/go.mod h1:8zZa+Al3WsESfmgSs98Fi06dRWLH5Bnq90m5bKD/eT4=
|
||||
github.com/nats-io/nats.go v1.15.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nats.go v1.16.0 h1:zvLE7fGBQYW6MWaFaRdsgm9qT39PJDQoju+DS8KsO1g=
|
||||
github.com/nats-io/nats.go v1.16.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/jwt/v2 v2.5.7 h1:j5lH1fUXCnJnY8SsQeB/a/z9Azgu2bYIDvtPVNdxe2c=
|
||||
github.com/nats-io/jwt/v2 v2.5.7/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
|
||||
github.com/nats-io/nats-server/v2 v2.10.16 h1:2jXaiydp5oB/nAx/Ytf9fdCi9QN6ItIc9eehX8kwVV0=
|
||||
github.com/nats-io/nats-server/v2 v2.10.16/go.mod h1:Pksi38H2+6xLe1vQx0/EA4bzetM0NqyIHcIbmgXSkIU=
|
||||
github.com/nats-io/nats.go v1.35.0 h1:XFNqNM7v5B+MQMKqVGAyHwYhyKb48jrenXNxIU20ULk=
|
||||
github.com/nats-io/nats.go v1.35.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/notedit/janus-go v0.0.0-20200517101215-10eb8b95d1a0 h1:EFU9iv8BMPyBo8iFMHvQleYlF5M3PY6zpAbxsngImjE=
|
||||
github.com/notedit/janus-go v0.0.0-20200517101215-10eb8b95d1a0/go.mod h1:BN/Txse3qz8tZOmCm2OfajB2wHVujWmX3o9nVdsI6gE=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
|
||||
github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pion/sdp v1.3.0 h1:21lpgEILHyolpsIrbCBagZaAPj4o057cFjzaFebkVOs=
|
||||
github.com/pion/sdp v1.3.0/go.mod h1:ceA2lTyftydQTuCIbUNoH77aAt6CiQJaRpssA4Gee8I=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
|
||||
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
|
||||
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao=
|
||||
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4=
|
||||
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.4 h1:V5Dvl7S39ZDwjkKqJG2BfXgxZ3QREqqKifWQgIw5IM0=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.4/go.mod h1:OI+TtO+Aa3nhQSppMbwE4ld3uF1/fqqwbpfndbbrEe0=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.4 h1:YGrnAgRfgXloBNuqa+oBI/aRZMcK/1GS6trJePJ/Gqc=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.4/go.mod h1:SCuunjYvZFC0fBX0vxMSPjuZmpcSk+XaAcMrD6Do03w=
|
||||
go.etcd.io/etcd/server/v3 v3.5.4 h1:CMAZd0g8Bn5NRhynW6pKhc4FRg41/0QYy3d7aNm9874=
|
||||
go.etcd.io/etcd/server/v3 v3.5.4/go.mod h1:S5/YTU15KxymM5l3T6b09sNOHPXqGYIZStpuuGbb65c=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
|
||||
go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 h1:sO4WKdPAudZGKPcpZT4MJn6JaDmpyLrMPDGGyA1SttE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=
|
||||
go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g=
|
||||
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg=
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
|
||||
go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8=
|
||||
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
|
||||
go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
|
||||
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8=
|
||||
go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g=
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=
|
||||
go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw=
|
||||
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=
|
||||
go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg=
|
||||
go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js=
|
||||
go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M=
|
||||
go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
|
||||
go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0=
|
||||
go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA=
|
||||
go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM=
|
||||
go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM=
|
||||
go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0=
|
||||
go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
|
||||
go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220325203850-36772127a21f h1:TrmogKRsSOxRMJbLYGrB4SBbW+LJcEllYBLME5Zk5pU=
|
||||
golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
|
38
grpc_backend.proto
Normal file
38
grpc_backend.proto
Normal file
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling";
|
||||
|
||||
package signaling;
|
||||
|
||||
service RpcBackend {
|
||||
rpc GetSessionCount(GetSessionCountRequest) returns (GetSessionCountReply) {}
|
||||
}
|
||||
|
||||
message GetSessionCountRequest {
|
||||
string url = 1;
|
||||
}
|
||||
|
||||
message GetSessionCountReply {
|
||||
uint32 count = 1;
|
||||
}
|
936
grpc_client.go
Normal file
936
grpc_client.go
Normal file
|
@ -0,0 +1,936 @@
|
|||
/**
|
||||
* 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"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/resolver"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
GrpcTargetTypeStatic = "static"
|
||||
GrpcTargetTypeEtcd = "etcd"
|
||||
|
||||
DefaultGrpcTargetType = GrpcTargetTypeStatic
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSuchResumeId = fmt.Errorf("unknown resume id")
|
||||
|
||||
customResolverPrefix atomic.Uint64
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGrpcClientStats()
|
||||
}
|
||||
|
||||
type grpcClientImpl struct {
|
||||
RpcBackendClient
|
||||
RpcInternalClient
|
||||
RpcMcuClient
|
||||
RpcSessionsClient
|
||||
}
|
||||
|
||||
func newGrpcClientImpl(conn grpc.ClientConnInterface) *grpcClientImpl {
|
||||
return &grpcClientImpl{
|
||||
RpcBackendClient: NewRpcBackendClient(conn),
|
||||
RpcInternalClient: NewRpcInternalClient(conn),
|
||||
RpcMcuClient: NewRpcMcuClient(conn),
|
||||
RpcSessionsClient: NewRpcSessionsClient(conn),
|
||||
}
|
||||
}
|
||||
|
||||
type GrpcClient struct {
|
||||
ip net.IP
|
||||
target string
|
||||
conn *grpc.ClientConn
|
||||
impl *grpcClientImpl
|
||||
|
||||
isSelf atomic.Bool
|
||||
}
|
||||
|
||||
type customIpResolver struct {
|
||||
resolver.Builder
|
||||
resolver.Resolver
|
||||
|
||||
scheme string
|
||||
addr string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func (r *customIpResolver) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
state := resolver.State{
|
||||
Addresses: []resolver.Address{
|
||||
{
|
||||
Addr: r.addr,
|
||||
ServerName: r.hostname,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := cc.UpdateState(state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *customIpResolver) Scheme() string {
|
||||
return r.scheme
|
||||
}
|
||||
|
||||
func (r *customIpResolver) ResolveNow(opts resolver.ResolveNowOptions) {
|
||||
// Noop, we use a static configuration.
|
||||
}
|
||||
|
||||
func (r *customIpResolver) Close() {
|
||||
// Noop
|
||||
}
|
||||
|
||||
func NewGrpcClient(target string, ip net.IP, opts ...grpc.DialOption) (*GrpcClient, error) {
|
||||
var conn *grpc.ClientConn
|
||||
var err error
|
||||
if ip != nil {
|
||||
prefix := customResolverPrefix.Add(1)
|
||||
addr := ip.String()
|
||||
hostname := target
|
||||
if host, port, err := net.SplitHostPort(target); err == nil {
|
||||
addr = net.JoinHostPort(addr, port)
|
||||
hostname = host
|
||||
}
|
||||
resolver := &customIpResolver{
|
||||
scheme: fmt.Sprintf("custom%d", prefix),
|
||||
addr: addr,
|
||||
hostname: hostname,
|
||||
}
|
||||
opts = append(opts, grpc.WithResolvers(resolver))
|
||||
conn, err = grpc.NewClient(fmt.Sprintf("%s://%s", resolver.Scheme(), target), opts...)
|
||||
} else {
|
||||
conn, err = grpc.NewClient(target, opts...)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GrpcClient{
|
||||
ip: ip,
|
||||
target: target,
|
||||
conn: conn,
|
||||
impl: newGrpcClientImpl(conn),
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
result.target += " (" + ip.String() + ")"
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) Target() string {
|
||||
return c.target
|
||||
}
|
||||
|
||||
func (c *GrpcClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *GrpcClient) IsSelf() bool {
|
||||
return c.isSelf.Load()
|
||||
}
|
||||
|
||||
func (c *GrpcClient) SetSelf(self bool) {
|
||||
c.isSelf.Store(self)
|
||||
}
|
||||
|
||||
func (c *GrpcClient) GetServerId(ctx context.Context) (string, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("GetServerId").Inc()
|
||||
response, err := c.impl.GetServerId(ctx, &GetServerIdRequest{}, grpc.WaitForReady(true))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response.GetServerId(), nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) LookupResumeId(ctx context.Context, resumeId string) (*LookupResumeIdReply, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("LookupResumeId").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Lookup resume id %s on %s", resumeId, c.Target())
|
||||
response, err := c.impl.LookupResumeId(ctx, &LookupResumeIdRequest{
|
||||
ResumeId: resumeId,
|
||||
}, grpc.WaitForReady(true))
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
return nil, ErrNoSuchResumeId
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sessionId := response.GetSessionId(); sessionId == "" {
|
||||
return nil, ErrNoSuchResumeId
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) LookupSessionId(ctx context.Context, roomSessionId string, disconnectReason string) (string, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("LookupSessionId").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Lookup room session %s on %s", roomSessionId, c.Target())
|
||||
response, err := c.impl.LookupSessionId(ctx, &LookupSessionIdRequest{
|
||||
RoomSessionId: roomSessionId,
|
||||
DisconnectReason: disconnectReason,
|
||||
}, grpc.WaitForReady(true))
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
return "", ErrNoSuchRoomSession
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sessionId := response.GetSessionId()
|
||||
if sessionId == "" {
|
||||
return "", ErrNoSuchRoomSession
|
||||
}
|
||||
|
||||
return sessionId, nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) IsSessionInCall(ctx context.Context, sessionId string, room *Room) (bool, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("IsSessionInCall").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Check if session %s is in call %s on %s", sessionId, room.Id(), c.Target())
|
||||
response, err := c.impl.IsSessionInCall(ctx, &IsSessionInCallRequest{
|
||||
SessionId: sessionId,
|
||||
RoomId: room.Id(),
|
||||
BackendUrl: room.Backend().url,
|
||||
}, grpc.WaitForReady(true))
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return response.GetInCall(), nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) GetPublisherId(ctx context.Context, sessionId string, streamType StreamType) (string, string, net.IP, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("GetPublisherId").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Get %s publisher id %s on %s", streamType, sessionId, c.Target())
|
||||
response, err := c.impl.GetPublisherId(ctx, &GetPublisherIdRequest{
|
||||
SessionId: sessionId,
|
||||
StreamType: string(streamType),
|
||||
}, grpc.WaitForReady(true))
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
return "", "", nil, nil
|
||||
} else if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
return response.GetPublisherId(), response.GetProxyUrl(), net.ParseIP(response.GetIp()), nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) GetSessionCount(ctx context.Context, u *url.URL) (uint32, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("GetSessionCount").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Get session count for %s on %s", u, c.Target())
|
||||
response, err := c.impl.GetSessionCount(ctx, &GetSessionCountRequest{
|
||||
Url: u.String(),
|
||||
}, grpc.WaitForReady(true))
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
return 0, nil
|
||||
} else if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return response.GetCount(), nil
|
||||
}
|
||||
|
||||
type ProxySessionReceiver interface {
|
||||
RemoteAddr() string
|
||||
Country() string
|
||||
UserAgent() string
|
||||
|
||||
OnProxyMessage(message *ServerSessionMessage) error
|
||||
OnProxyClose(err error)
|
||||
}
|
||||
|
||||
type SessionProxy struct {
|
||||
sessionId string
|
||||
receiver ProxySessionReceiver
|
||||
|
||||
sendMu sync.Mutex
|
||||
client RpcSessions_ProxySessionClient
|
||||
}
|
||||
|
||||
func (p *SessionProxy) recvPump() {
|
||||
var closeError error
|
||||
defer func() {
|
||||
p.receiver.OnProxyClose(closeError)
|
||||
if err := p.Close(); err != nil {
|
||||
log.Printf("Error closing proxy for session %s: %s", p.sessionId, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
msg, err := p.client.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("Error receiving message from proxy for session %s: %s", p.sessionId, err)
|
||||
closeError = err
|
||||
break
|
||||
}
|
||||
|
||||
if err := p.receiver.OnProxyMessage(msg); err != nil {
|
||||
log.Printf("Error processing message %+v from proxy for session %s: %s", msg, p.sessionId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SessionProxy) Send(message *ClientSessionMessage) error {
|
||||
p.sendMu.Lock()
|
||||
defer p.sendMu.Unlock()
|
||||
return p.client.Send(message)
|
||||
}
|
||||
|
||||
func (p *SessionProxy) Close() error {
|
||||
p.sendMu.Lock()
|
||||
defer p.sendMu.Unlock()
|
||||
return p.client.CloseSend()
|
||||
}
|
||||
|
||||
func (c *GrpcClient) ProxySession(ctx context.Context, sessionId string, receiver ProxySessionReceiver) (*SessionProxy, error) {
|
||||
statsGrpcClientCalls.WithLabelValues("ProxySession").Inc()
|
||||
md := metadata.Pairs(
|
||||
"sessionId", sessionId,
|
||||
"remoteAddr", receiver.RemoteAddr(),
|
||||
"country", receiver.Country(),
|
||||
"userAgent", receiver.UserAgent(),
|
||||
)
|
||||
client, err := c.impl.ProxySession(metadata.NewOutgoingContext(ctx, md), grpc.WaitForReady(true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := &SessionProxy{
|
||||
sessionId: sessionId,
|
||||
receiver: receiver,
|
||||
|
||||
client: client,
|
||||
}
|
||||
|
||||
go proxy.recvPump()
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
type grpcClientsList struct {
|
||||
clients []*GrpcClient
|
||||
entry *DnsMonitorEntry
|
||||
}
|
||||
|
||||
type GrpcClients struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
clientsMap map[string]*grpcClientsList
|
||||
clients []*GrpcClient
|
||||
|
||||
dnsMonitor *DnsMonitor
|
||||
dnsDiscovery bool
|
||||
|
||||
etcdClient *EtcdClient
|
||||
targetPrefix string
|
||||
targetInformation map[string]*GrpcTargetInformationEtcd
|
||||
dialOptions atomic.Value // []grpc.DialOption
|
||||
creds credentials.TransportCredentials
|
||||
|
||||
initializedCtx context.Context
|
||||
initializedFunc context.CancelFunc
|
||||
wakeupChanForTesting chan struct{}
|
||||
selfCheckWaitGroup sync.WaitGroup
|
||||
|
||||
closeCtx context.Context
|
||||
closeFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewGrpcClients(config *goconf.ConfigFile, etcdClient *EtcdClient, dnsMonitor *DnsMonitor) (*GrpcClients, error) {
|
||||
initializedCtx, initializedFunc := context.WithCancel(context.Background())
|
||||
closeCtx, closeFunc := context.WithCancel(context.Background())
|
||||
result := &GrpcClients{
|
||||
dnsMonitor: dnsMonitor,
|
||||
etcdClient: etcdClient,
|
||||
initializedCtx: initializedCtx,
|
||||
initializedFunc: initializedFunc,
|
||||
closeCtx: closeCtx,
|
||||
closeFunc: closeFunc,
|
||||
}
|
||||
if err := result.load(config, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *GrpcClients) load(config *goconf.ConfigFile, fromReload bool) error {
|
||||
creds, err := NewReloadableCredentials(config, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.creds != nil {
|
||||
if cr, ok := c.creds.(*reloadableCredentials); ok {
|
||||
cr.Close()
|
||||
}
|
||||
}
|
||||
c.creds = creds
|
||||
|
||||
opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)}
|
||||
c.dialOptions.Store(opts)
|
||||
|
||||
targetType, _ := config.GetString("grpc", "targettype")
|
||||
if targetType == "" {
|
||||
targetType = DefaultGrpcTargetType
|
||||
}
|
||||
|
||||
switch targetType {
|
||||
case GrpcTargetTypeStatic:
|
||||
err = c.loadTargetsStatic(config, fromReload, opts...)
|
||||
case GrpcTargetTypeEtcd:
|
||||
err = c.loadTargetsEtcd(config, fromReload, opts...)
|
||||
default:
|
||||
err = fmt.Errorf("unknown GRPC target type: %s", targetType)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GrpcClients) closeClient(client *GrpcClient) {
|
||||
if client.IsSelf() {
|
||||
// Already closed.
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Close(); err != nil {
|
||||
log.Printf("Error closing client to %s: %s", client.Target(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClients) isClientAvailable(target string, client *GrpcClient) bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entries, found := c.clientsMap[target]
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, entry := range entries.clients {
|
||||
if entry == client {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *GrpcClients) getServerIdWithTimeout(ctx context.Context, client *GrpcClient) (string, error) {
|
||||
ctx2, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := client.GetServerId(ctx2)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (c *GrpcClients) checkIsSelf(ctx context.Context, target string, client *GrpcClient) {
|
||||
backoff, _ := NewExponentialBackoff(initialWaitDelay, maxWaitDelay)
|
||||
defer c.selfCheckWaitGroup.Done()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Cancelled
|
||||
return
|
||||
default:
|
||||
if !c.isClientAvailable(target, client) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := c.getServerIdWithTimeout(ctx, client)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
if status.Code(err) != codes.Canceled {
|
||||
log.Printf("Error checking GRPC server id of %s, retrying in %s: %s", client.Target(), backoff.NextWait(), err)
|
||||
}
|
||||
backoff.Wait(ctx)
|
||||
continue
|
||||
}
|
||||
|
||||
if id == GrpcServerId {
|
||||
log.Printf("GRPC target %s is this server, removing", client.Target())
|
||||
c.closeClient(client)
|
||||
client.SetSelf(true)
|
||||
} else {
|
||||
log.Printf("Checked GRPC server id of %s", client.Target())
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, opts ...grpc.DialOption) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
dnsDiscovery, _ := config.GetBool("grpc", "dnsdiscovery")
|
||||
if dnsDiscovery != c.dnsDiscovery {
|
||||
if !dnsDiscovery {
|
||||
for _, entry := range c.clientsMap {
|
||||
if entry.entry != nil {
|
||||
c.dnsMonitor.Remove(entry.entry)
|
||||
entry.entry = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
c.dnsDiscovery = dnsDiscovery
|
||||
}
|
||||
|
||||
clientsMap := make(map[string]*grpcClientsList)
|
||||
var clients []*GrpcClient
|
||||
removeTargets := make(map[string]bool, len(c.clientsMap))
|
||||
for target, entries := range c.clientsMap {
|
||||
removeTargets[target] = true
|
||||
clientsMap[target] = entries
|
||||
}
|
||||
|
||||
targets, _ := config.GetString("grpc", "targets")
|
||||
for _, target := range strings.Split(targets, ",") {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if entries, found := clientsMap[target]; found {
|
||||
clients = append(clients, entries.clients...)
|
||||
if dnsDiscovery && entries.entry == nil {
|
||||
entry, err := c.dnsMonitor.Add(target, c.onLookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries.entry = entry
|
||||
}
|
||||
delete(removeTargets, target)
|
||||
continue
|
||||
}
|
||||
|
||||
host := target
|
||||
if h, _, err := net.SplitHostPort(target); err == nil {
|
||||
host = h
|
||||
}
|
||||
|
||||
if dnsDiscovery && net.ParseIP(host) == nil {
|
||||
// Use dedicated client for each IP address.
|
||||
entry, err := c.dnsMonitor.Add(target, c.onLookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientsMap[target] = &grpcClientsList{
|
||||
entry: entry,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
client, err := NewGrpcClient(target, nil, opts...)
|
||||
if err != nil {
|
||||
for _, entry := range clientsMap {
|
||||
for _, client := range entry.clients {
|
||||
c.closeClient(client)
|
||||
}
|
||||
|
||||
if entry.entry != nil {
|
||||
c.dnsMonitor.Remove(entry.entry)
|
||||
entry.entry = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
c.selfCheckWaitGroup.Add(1)
|
||||
go c.checkIsSelf(c.closeCtx, target, client)
|
||||
|
||||
log.Printf("Adding %s as GRPC target", client.Target())
|
||||
entry, found := clientsMap[target]
|
||||
if !found {
|
||||
entry = &grpcClientsList{}
|
||||
clientsMap[target] = entry
|
||||
}
|
||||
entry.clients = append(entry.clients, client)
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
for target := range removeTargets {
|
||||
if entry, found := clientsMap[target]; found {
|
||||
for _, client := range entry.clients {
|
||||
log.Printf("Deleting GRPC target %s", client.Target())
|
||||
c.closeClient(client)
|
||||
}
|
||||
|
||||
if entry.entry != nil {
|
||||
c.dnsMonitor.Remove(entry.entry)
|
||||
entry.entry = nil
|
||||
}
|
||||
delete(clientsMap, target)
|
||||
}
|
||||
}
|
||||
|
||||
c.clients = clients
|
||||
c.clientsMap = clientsMap
|
||||
c.initializedFunc()
|
||||
statsGrpcClients.Set(float64(len(clients)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GrpcClients) onLookup(entry *DnsMonitorEntry, all []net.IP, added []net.IP, keep []net.IP, removed []net.IP) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
target := entry.URL()
|
||||
e, found := c.clientsMap[target]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
opts := c.dialOptions.Load().([]grpc.DialOption)
|
||||
|
||||
mapModified := false
|
||||
var newClients []*GrpcClient
|
||||
for _, ip := range removed {
|
||||
for _, client := range e.clients {
|
||||
if ip.Equal(client.ip) {
|
||||
mapModified = true
|
||||
log.Printf("Removing connection to %s", client.Target())
|
||||
c.closeClient(client)
|
||||
c.wakeupForTesting()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ip := range keep {
|
||||
for _, client := range e.clients {
|
||||
if ip.Equal(client.ip) {
|
||||
newClients = append(newClients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ip := range added {
|
||||
client, err := NewGrpcClient(target, ip, opts...)
|
||||
if err != nil {
|
||||
log.Printf("Error creating client to %s with IP %s: %s", target, ip.String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
c.selfCheckWaitGroup.Add(1)
|
||||
go c.checkIsSelf(c.closeCtx, target, client)
|
||||
|
||||
log.Printf("Adding %s as GRPC target", client.Target())
|
||||
newClients = append(newClients, client)
|
||||
mapModified = true
|
||||
c.wakeupForTesting()
|
||||
}
|
||||
|
||||
if mapModified {
|
||||
c.clientsMap[target].clients = newClients
|
||||
|
||||
c.clients = make([]*GrpcClient, 0, len(c.clientsMap))
|
||||
for _, entry := range c.clientsMap {
|
||||
c.clients = append(c.clients, entry.clients...)
|
||||
}
|
||||
statsGrpcClients.Set(float64(len(c.clients)))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClients) loadTargetsEtcd(config *goconf.ConfigFile, fromReload bool, opts ...grpc.DialOption) error {
|
||||
if !c.etcdClient.IsConfigured() {
|
||||
return fmt.Errorf("No etcd endpoints configured")
|
||||
}
|
||||
|
||||
targetPrefix, _ := config.GetString("grpc", "targetprefix")
|
||||
if targetPrefix == "" {
|
||||
return fmt.Errorf("No GRPC target prefix configured")
|
||||
}
|
||||
c.targetPrefix = targetPrefix
|
||||
if c.targetInformation == nil {
|
||||
c.targetInformation = make(map[string]*GrpcTargetInformationEtcd)
|
||||
}
|
||||
|
||||
c.etcdClient.AddListener(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GrpcClients) EtcdClientCreated(client *EtcdClient) {
|
||||
go func() {
|
||||
if err := client.WaitForConnection(c.closeCtx); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
panic(err)
|
||||
}
|
||||
|
||||
backoff, _ := NewExponentialBackoff(initialWaitDelay, maxWaitDelay)
|
||||
var nextRevision int64
|
||||
for c.closeCtx.Err() == nil {
|
||||
response, err := c.getGrpcTargets(c.closeCtx, client, c.targetPrefix)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Printf("Timeout getting initial list of GRPC targets, retry in %s", backoff.NextWait())
|
||||
} else {
|
||||
log.Printf("Could not get initial list of GRPC targets, retry in %s: %s", backoff.NextWait(), err)
|
||||
}
|
||||
|
||||
backoff.Wait(c.closeCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ev := range response.Kvs {
|
||||
c.EtcdKeyUpdated(client, string(ev.Key), ev.Value, nil)
|
||||
}
|
||||
c.initializedFunc()
|
||||
nextRevision = response.Header.Revision + 1
|
||||
break
|
||||
}
|
||||
|
||||
prevRevision := nextRevision
|
||||
backoff.Reset()
|
||||
for c.closeCtx.Err() == nil {
|
||||
var err error
|
||||
if nextRevision, err = client.Watch(c.closeCtx, c.targetPrefix, nextRevision, c, clientv3.WithPrefix()); err != nil {
|
||||
log.Printf("Error processing watch for %s (%s), retry in %s", c.targetPrefix, err, backoff.NextWait())
|
||||
backoff.Wait(c.closeCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
if nextRevision != prevRevision {
|
||||
backoff.Reset()
|
||||
prevRevision = nextRevision
|
||||
} else {
|
||||
log.Printf("Processing watch for %s interrupted, retry in %s", c.targetPrefix, backoff.NextWait())
|
||||
backoff.Wait(c.closeCtx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *GrpcClients) EtcdWatchCreated(client *EtcdClient, key string) {
|
||||
}
|
||||
|
||||
func (c *GrpcClients) getGrpcTargets(ctx context.Context, client *EtcdClient, targetPrefix string) (*clientv3.GetResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
return client.Get(ctx, targetPrefix, clientv3.WithPrefix())
|
||||
}
|
||||
|
||||
func (c *GrpcClients) EtcdKeyUpdated(client *EtcdClient, key string, data []byte, prevValue []byte) {
|
||||
var info GrpcTargetInformationEtcd
|
||||
if err := json.Unmarshal(data, &info); err != nil {
|
||||
log.Printf("Could not decode GRPC target %s=%s: %s", key, string(data), err)
|
||||
return
|
||||
}
|
||||
if err := info.CheckValid(); err != nil {
|
||||
log.Printf("Received invalid GRPC target %s=%s: %s", key, string(data), err)
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
prev, found := c.targetInformation[key]
|
||||
if found && prev.Address != info.Address {
|
||||
// Address of endpoint has changed, remove old one.
|
||||
c.removeEtcdClientLocked(key)
|
||||
}
|
||||
|
||||
if _, found := c.clientsMap[info.Address]; found {
|
||||
log.Printf("GRPC target %s already exists, ignoring %s", info.Address, key)
|
||||
return
|
||||
}
|
||||
|
||||
opts := c.dialOptions.Load().([]grpc.DialOption)
|
||||
cl, err := NewGrpcClient(info.Address, nil, opts...)
|
||||
if err != nil {
|
||||
log.Printf("Could not create GRPC client for target %s: %s", info.Address, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.selfCheckWaitGroup.Add(1)
|
||||
go c.checkIsSelf(c.closeCtx, info.Address, cl)
|
||||
|
||||
log.Printf("Adding %s as GRPC target", cl.Target())
|
||||
|
||||
if c.clientsMap == nil {
|
||||
c.clientsMap = make(map[string]*grpcClientsList)
|
||||
}
|
||||
c.clientsMap[info.Address] = &grpcClientsList{
|
||||
clients: []*GrpcClient{cl},
|
||||
}
|
||||
c.clients = append(c.clients, cl)
|
||||
c.targetInformation[key] = &info
|
||||
statsGrpcClients.Inc()
|
||||
c.wakeupForTesting()
|
||||
}
|
||||
|
||||
func (c *GrpcClients) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.removeEtcdClientLocked(key)
|
||||
}
|
||||
|
||||
func (c *GrpcClients) removeEtcdClientLocked(key string) {
|
||||
info, found := c.targetInformation[key]
|
||||
if !found {
|
||||
log.Printf("No connection found for %s, ignoring", key)
|
||||
c.wakeupForTesting()
|
||||
return
|
||||
}
|
||||
|
||||
delete(c.targetInformation, key)
|
||||
entry, found := c.clientsMap[info.Address]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
for _, client := range entry.clients {
|
||||
log.Printf("Removing connection to %s (from %s)", client.Target(), key)
|
||||
c.closeClient(client)
|
||||
}
|
||||
delete(c.clientsMap, info.Address)
|
||||
c.clients = make([]*GrpcClient, 0, len(c.clientsMap))
|
||||
for _, entry := range c.clientsMap {
|
||||
c.clients = append(c.clients, entry.clients...)
|
||||
}
|
||||
statsGrpcClients.Dec()
|
||||
c.wakeupForTesting()
|
||||
}
|
||||
|
||||
func (c *GrpcClients) WaitForInitialized(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.initializedCtx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClients) wakeupForTesting() {
|
||||
if c.wakeupChanForTesting == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case c.wakeupChanForTesting <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClients) Reload(config *goconf.ConfigFile) {
|
||||
if err := c.load(config, true); err != nil {
|
||||
log.Printf("Could not reload RPC clients: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClients) Close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for _, entry := range c.clientsMap {
|
||||
for _, client := range entry.clients {
|
||||
if err := client.Close(); err != nil {
|
||||
log.Printf("Error closing client to %s: %s", client.Target(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if entry.entry != nil {
|
||||
c.dnsMonitor.Remove(entry.entry)
|
||||
entry.entry = nil
|
||||
}
|
||||
}
|
||||
|
||||
c.clients = nil
|
||||
c.clientsMap = nil
|
||||
c.dnsDiscovery = false
|
||||
|
||||
if c.etcdClient != nil {
|
||||
c.etcdClient.RemoveListener(c)
|
||||
}
|
||||
if c.creds != nil {
|
||||
if cr, ok := c.creds.(*reloadableCredentials); ok {
|
||||
cr.Close()
|
||||
}
|
||||
}
|
||||
c.closeFunc()
|
||||
}
|
||||
|
||||
func (c *GrpcClients) GetClients() []*GrpcClient {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if len(c.clients) == 0 {
|
||||
return c.clients
|
||||
}
|
||||
|
||||
result := make([]*GrpcClient, 0, len(c.clients)-1)
|
||||
for _, client := range c.clients {
|
||||
if client.IsSelf() {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, client)
|
||||
}
|
||||
return result
|
||||
}
|
389
grpc_client_test.go
Normal file
389
grpc_client_test.go
Normal file
|
@ -0,0 +1,389 @@
|
|||
/**
|
||||
* 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"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"go.etcd.io/etcd/server/v3/embed"
|
||||
)
|
||||
|
||||
func (c *GrpcClients) getWakeupChannelForTesting() <-chan struct{} {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.wakeupChanForTesting != nil {
|
||||
return c.wakeupChanForTesting
|
||||
}
|
||||
|
||||
ch := make(chan struct{}, 1)
|
||||
c.wakeupChanForTesting = ch
|
||||
return ch
|
||||
}
|
||||
|
||||
func NewGrpcClientsForTestWithConfig(t *testing.T, config *goconf.ConfigFile, etcdClient *EtcdClient) (*GrpcClients, *DnsMonitor) {
|
||||
dnsMonitor := newDnsMonitorForTest(t, time.Hour) // will be updated manually
|
||||
client, err := NewGrpcClients(config, etcdClient, dnsMonitor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
client.Close()
|
||||
})
|
||||
|
||||
return client, dnsMonitor
|
||||
}
|
||||
|
||||
func NewGrpcClientsForTest(t *testing.T, addr string) (*GrpcClients, *DnsMonitor) {
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("grpc", "targets", addr)
|
||||
config.AddOption("grpc", "dnsdiscovery", "true")
|
||||
|
||||
return NewGrpcClientsForTestWithConfig(t, config, nil)
|
||||
}
|
||||
|
||||
func NewGrpcClientsWithEtcdForTest(t *testing.T, etcd *embed.Etcd) (*GrpcClients, *DnsMonitor) {
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String())
|
||||
|
||||
config.AddOption("grpc", "targettype", "etcd")
|
||||
config.AddOption("grpc", "targetprefix", "/grpctargets")
|
||||
|
||||
etcdClient, err := NewEtcdClient(config, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := etcdClient.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
return NewGrpcClientsForTestWithConfig(t, config, etcdClient)
|
||||
}
|
||||
|
||||
func drainWakeupChannel(ch <-chan struct{}) {
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForEvent(ctx context.Context, t *testing.T, ch <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
t.Error("timeout waiting for event")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GrpcClients_EtcdInitial(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
ensureNoGoroutinesLeak(t, func(t *testing.T) {
|
||||
_, addr1 := NewGrpcServerForTest(t)
|
||||
_, addr2 := NewGrpcServerForTest(t)
|
||||
|
||||
etcd := NewEtcdForTest(t)
|
||||
|
||||
SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}"))
|
||||
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}"))
|
||||
|
||||
client, _ := NewGrpcClientsWithEtcdForTest(t, etcd)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := client.WaitForInitialized(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 2 {
|
||||
t.Errorf("Expected two clients, got %+v", clients)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GrpcClients_EtcdUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
etcd := NewEtcdForTest(t)
|
||||
client, _ := NewGrpcClientsWithEtcdForTest(t, etcd)
|
||||
ch := client.getWakeupChannelForTesting()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 0 {
|
||||
t.Errorf("Expected no clients, got %+v", clients)
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
_, addr1 := NewGrpcServerForTest(t)
|
||||
SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}"))
|
||||
waitForEvent(ctx, t, ch)
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != addr1 {
|
||||
t.Errorf("Expected target %s, got %s", addr1, clients[0].Target())
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
_, addr2 := NewGrpcServerForTest(t)
|
||||
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}"))
|
||||
waitForEvent(ctx, t, ch)
|
||||
if clients := client.GetClients(); len(clients) != 2 {
|
||||
t.Errorf("Expected two clients, got %+v", clients)
|
||||
} else if clients[0].Target() != addr1 {
|
||||
t.Errorf("Expected target %s, got %s", addr1, clients[0].Target())
|
||||
} else if clients[1].Target() != addr2 {
|
||||
t.Errorf("Expected target %s, got %s", addr2, clients[1].Target())
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
DeleteEtcdValue(etcd, "/grpctargets/one")
|
||||
waitForEvent(ctx, t, ch)
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != addr2 {
|
||||
t.Errorf("Expected target %s, got %s", addr2, clients[0].Target())
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
_, addr3 := NewGrpcServerForTest(t)
|
||||
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr3+"\"}"))
|
||||
waitForEvent(ctx, t, ch)
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != addr3 {
|
||||
t.Errorf("Expected target %s, got %s", addr3, clients[0].Target())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GrpcClients_EtcdIgnoreSelf(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
etcd := NewEtcdForTest(t)
|
||||
client, _ := NewGrpcClientsWithEtcdForTest(t, etcd)
|
||||
ch := client.getWakeupChannelForTesting()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 0 {
|
||||
t.Errorf("Expected no clients, got %+v", clients)
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
_, addr1 := NewGrpcServerForTest(t)
|
||||
SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}"))
|
||||
waitForEvent(ctx, t, ch)
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != addr1 {
|
||||
t.Errorf("Expected target %s, got %s", addr1, clients[0].Target())
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
server2, addr2 := NewGrpcServerForTest(t)
|
||||
server2.serverId = GrpcServerId
|
||||
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}"))
|
||||
waitForEvent(ctx, t, ch)
|
||||
client.selfCheckWaitGroup.Wait()
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != addr1 {
|
||||
t.Errorf("Expected target %s, got %s", addr1, clients[0].Target())
|
||||
}
|
||||
|
||||
drainWakeupChannel(ch)
|
||||
DeleteEtcdValue(etcd, "/grpctargets/two")
|
||||
waitForEvent(ctx, t, ch)
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != addr1 {
|
||||
t.Errorf("Expected target %s, got %s", addr1, clients[0].Target())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GrpcClients_DnsDiscovery(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
ensureNoGoroutinesLeak(t, func(t *testing.T) {
|
||||
lookup := newMockDnsLookupForTest(t)
|
||||
target := "testgrpc:12345"
|
||||
ip1 := net.ParseIP("192.168.0.1")
|
||||
ip2 := net.ParseIP("192.168.0.2")
|
||||
targetWithIp1 := fmt.Sprintf("%s (%s)", target, ip1)
|
||||
targetWithIp2 := fmt.Sprintf("%s (%s)", target, ip2)
|
||||
lookup.Set("testgrpc", []net.IP{ip1})
|
||||
client, dnsMonitor := NewGrpcClientsForTest(t, target)
|
||||
ch := client.getWakeupChannelForTesting()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
dnsMonitor.checkHostnames()
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != targetWithIp1 {
|
||||
t.Errorf("Expected target %s, got %s", targetWithIp1, clients[0].Target())
|
||||
} else if !clients[0].ip.Equal(ip1) {
|
||||
t.Errorf("Expected IP %s, got %s", ip1, clients[0].ip)
|
||||
}
|
||||
|
||||
lookup.Set("testgrpc", []net.IP{ip1, ip2})
|
||||
drainWakeupChannel(ch)
|
||||
dnsMonitor.checkHostnames()
|
||||
waitForEvent(ctx, t, ch)
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 2 {
|
||||
t.Errorf("Expected two client, got %+v", clients)
|
||||
} else if clients[0].Target() != targetWithIp1 {
|
||||
t.Errorf("Expected target %s, got %s", targetWithIp1, clients[0].Target())
|
||||
} else if !clients[0].ip.Equal(ip1) {
|
||||
t.Errorf("Expected IP %s, got %s", ip1, clients[0].ip)
|
||||
} else if clients[1].Target() != targetWithIp2 {
|
||||
t.Errorf("Expected target %s, got %s", targetWithIp2, clients[1].Target())
|
||||
} else if !clients[1].ip.Equal(ip2) {
|
||||
t.Errorf("Expected IP %s, got %s", ip2, clients[1].ip)
|
||||
}
|
||||
|
||||
lookup.Set("testgrpc", []net.IP{ip2})
|
||||
drainWakeupChannel(ch)
|
||||
dnsMonitor.checkHostnames()
|
||||
waitForEvent(ctx, t, ch)
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != targetWithIp2 {
|
||||
t.Errorf("Expected target %s, got %s", targetWithIp2, clients[0].Target())
|
||||
} else if !clients[0].ip.Equal(ip2) {
|
||||
t.Errorf("Expected IP %s, got %s", ip2, clients[0].ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GrpcClients_DnsDiscoveryInitialFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
CatchLogForTest(t)
|
||||
lookup := newMockDnsLookupForTest(t)
|
||||
target := "testgrpc:12345"
|
||||
ip1 := net.ParseIP("192.168.0.1")
|
||||
targetWithIp1 := fmt.Sprintf("%s (%s)", target, ip1)
|
||||
client, dnsMonitor := NewGrpcClientsForTest(t, target)
|
||||
ch := client.getWakeupChannelForTesting()
|
||||
|
||||
testCtx, testCtxCancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer testCtxCancel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := client.WaitForInitialized(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 0 {
|
||||
t.Errorf("Expected no client, got %+v", clients)
|
||||
}
|
||||
|
||||
lookup.Set("testgrpc", []net.IP{ip1})
|
||||
drainWakeupChannel(ch)
|
||||
dnsMonitor.checkHostnames()
|
||||
waitForEvent(testCtx, t, ch)
|
||||
|
||||
if clients := client.GetClients(); len(clients) != 1 {
|
||||
t.Errorf("Expected one client, got %+v", clients)
|
||||
} else if clients[0].Target() != targetWithIp1 {
|
||||
t.Errorf("Expected target %s, got %s", targetWithIp1, clients[0].Target())
|
||||
} else if !clients[0].ip.Equal(ip1) {
|
||||
t.Errorf("Expected IP %s, got %s", ip1, clients[0].ip)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GrpcClients_Encryption(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
ensureNoGoroutinesLeak(t, func(t *testing.T) {
|
||||
serverKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serverCert := GenerateSelfSignedCertificateForTesting(t, 1024, "Server cert", serverKey)
|
||||
clientCert := GenerateSelfSignedCertificateForTesting(t, 1024, "Testing client", clientKey)
|
||||
|
||||
dir := t.TempDir()
|
||||
serverPrivkeyFile := path.Join(dir, "server-privkey.pem")
|
||||
serverPubkeyFile := path.Join(dir, "server-pubkey.pem")
|
||||
serverCertFile := path.Join(dir, "server-cert.pem")
|
||||
WritePrivateKey(serverKey, serverPrivkeyFile) // nolint
|
||||
WritePublicKey(&serverKey.PublicKey, serverPubkeyFile) // nolint
|
||||
os.WriteFile(serverCertFile, serverCert, 0755) // nolint
|
||||
clientPrivkeyFile := path.Join(dir, "client-privkey.pem")
|
||||
clientPubkeyFile := path.Join(dir, "client-pubkey.pem")
|
||||
clientCertFile := path.Join(dir, "client-cert.pem")
|
||||
WritePrivateKey(clientKey, clientPrivkeyFile) // nolint
|
||||
WritePublicKey(&clientKey.PublicKey, clientPubkeyFile) // nolint
|
||||
os.WriteFile(clientCertFile, clientCert, 0755) // nolint
|
||||
|
||||
serverConfig := goconf.NewConfigFile()
|
||||
serverConfig.AddOption("grpc", "servercertificate", serverCertFile)
|
||||
serverConfig.AddOption("grpc", "serverkey", serverPrivkeyFile)
|
||||
serverConfig.AddOption("grpc", "clientca", clientCertFile)
|
||||
_, addr := NewGrpcServerForTestWithConfig(t, serverConfig)
|
||||
|
||||
clientConfig := goconf.NewConfigFile()
|
||||
clientConfig.AddOption("grpc", "targets", addr)
|
||||
clientConfig.AddOption("grpc", "clientcertificate", clientCertFile)
|
||||
clientConfig.AddOption("grpc", "clientkey", clientPrivkeyFile)
|
||||
clientConfig.AddOption("grpc", "serverca", serverCertFile)
|
||||
clients, _ := NewGrpcClientsForTestWithConfig(t, clientConfig, nil)
|
||||
|
||||
ctx, cancel1 := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel1()
|
||||
|
||||
if err := clients.WaitForInitialized(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, client := range clients.GetClients() {
|
||||
if _, err := client.GetServerId(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
189
grpc_common.go
Normal file
189
grpc_common.go
Normal file
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* 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"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type reloadableCredentials struct {
|
||||
config *tls.Config
|
||||
|
||||
loader *CertificateReloader
|
||||
pool *CertPoolReloader
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
// use local cfg to avoid clobbering ServerName if using multiple endpoints
|
||||
cfg := c.config.Clone()
|
||||
if c.loader != nil {
|
||||
cfg.GetClientCertificate = c.loader.GetClientCertificate
|
||||
}
|
||||
if c.pool != nil {
|
||||
cfg.RootCAs = c.pool.GetCertPool()
|
||||
}
|
||||
if cfg.ServerName == "" {
|
||||
serverName, _, err := net.SplitHostPort(authority)
|
||||
if err != nil {
|
||||
// If the authority had no host port or if the authority cannot be parsed, use it as-is.
|
||||
serverName = authority
|
||||
}
|
||||
cfg.ServerName = serverName
|
||||
}
|
||||
conn := tls.Client(rawConn, cfg)
|
||||
errChannel := make(chan error, 1)
|
||||
go func() {
|
||||
errChannel <- conn.Handshake()
|
||||
close(errChannel)
|
||||
}()
|
||||
select {
|
||||
case err := <-errChannel:
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
conn.Close()
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
tlsInfo := credentials.TLSInfo{
|
||||
State: conn.ConnectionState(),
|
||||
CommonAuthInfo: credentials.CommonAuthInfo{
|
||||
SecurityLevel: credentials.PrivacyAndIntegrity,
|
||||
},
|
||||
}
|
||||
return WrapSyscallConn(rawConn, conn), tlsInfo, nil
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
cfg := c.config.Clone()
|
||||
if c.loader != nil {
|
||||
cfg.GetCertificate = c.loader.GetCertificate
|
||||
}
|
||||
if c.pool != nil {
|
||||
cfg.ClientCAs = c.pool.GetCertPool()
|
||||
}
|
||||
|
||||
conn := tls.Server(rawConn, cfg)
|
||||
if err := conn.Handshake(); err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
tlsInfo := credentials.TLSInfo{
|
||||
State: conn.ConnectionState(),
|
||||
CommonAuthInfo: credentials.CommonAuthInfo{
|
||||
SecurityLevel: credentials.PrivacyAndIntegrity,
|
||||
},
|
||||
}
|
||||
return WrapSyscallConn(rawConn, conn), tlsInfo, nil
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) Info() credentials.ProtocolInfo {
|
||||
return credentials.ProtocolInfo{
|
||||
SecurityProtocol: "tls",
|
||||
SecurityVersion: "1.2",
|
||||
ServerName: c.config.ServerName,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) Clone() credentials.TransportCredentials {
|
||||
return &reloadableCredentials{
|
||||
config: c.config.Clone(),
|
||||
pool: c.pool,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) OverrideServerName(serverName string) error {
|
||||
c.config.ServerName = serverName
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) Close() {
|
||||
if c.loader != nil {
|
||||
c.loader.Close()
|
||||
}
|
||||
if c.pool != nil {
|
||||
c.pool.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func NewReloadableCredentials(config *goconf.ConfigFile, server bool) (credentials.TransportCredentials, error) {
|
||||
var prefix string
|
||||
var caPrefix string
|
||||
if server {
|
||||
prefix = "server"
|
||||
caPrefix = "client"
|
||||
} else {
|
||||
prefix = "client"
|
||||
caPrefix = "server"
|
||||
}
|
||||
certificateFile, _ := config.GetString("grpc", prefix+"certificate")
|
||||
keyFile, _ := config.GetString("grpc", prefix+"key")
|
||||
caFile, _ := config.GetString("grpc", caPrefix+"ca")
|
||||
cfg := &tls.Config{
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
var loader *CertificateReloader
|
||||
var err error
|
||||
if certificateFile != "" && keyFile != "" {
|
||||
loader, err = NewCertificateReloader(certificateFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid GRPC %s certificate / key in %s / %s: %w", prefix, certificateFile, keyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
var pool *CertPoolReloader
|
||||
if caFile != "" {
|
||||
pool, err = NewCertPoolReloader(caFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if server {
|
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
}
|
||||
|
||||
if loader == nil && pool == nil {
|
||||
if server {
|
||||
log.Printf("WARNING: No GRPC server certificate and/or key configured, running unencrypted")
|
||||
} else {
|
||||
log.Printf("WARNING: No GRPC CA configured, expecting unencrypted connections")
|
||||
}
|
||||
return insecure.NewCredentials(), nil
|
||||
}
|
||||
|
||||
creds := &reloadableCredentials{
|
||||
config: cfg,
|
||||
loader: loader,
|
||||
pool: pool,
|
||||
}
|
||||
return creds, nil
|
||||
}
|
136
grpc_common_test.go
Normal file
136
grpc_common_test.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* 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"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *reloadableCredentials) WaitForCertificateReload(ctx context.Context) error {
|
||||
if c.loader == nil {
|
||||
return errors.New("no certificate loaded")
|
||||
}
|
||||
|
||||
return c.loader.WaitForReload(ctx)
|
||||
}
|
||||
|
||||
func (c *reloadableCredentials) WaitForCertPoolReload(ctx context.Context) error {
|
||||
if c.pool == nil {
|
||||
return errors.New("no certificate pool loaded")
|
||||
}
|
||||
|
||||
return c.pool.WaitForReload(ctx)
|
||||
}
|
||||
|
||||
func GenerateSelfSignedCertificateForTesting(t *testing.T, bits int, organization string, key *rsa.PrivateKey) []byte {
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{organization},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 180),
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
|
||||
data, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data = pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: data,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
func WritePrivateKey(key *rsa.PrivateKey, filename string) error {
|
||||
data := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
})
|
||||
|
||||
return os.WriteFile(filename, data, 0600)
|
||||
}
|
||||
|
||||
func WritePublicKey(key *rsa.PublicKey, filename string) error {
|
||||
data, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: data,
|
||||
})
|
||||
|
||||
return os.WriteFile(filename, data, 0755)
|
||||
}
|
||||
|
||||
func replaceFile(t *testing.T, filename string, data []byte, perm fs.FileMode) {
|
||||
t.Helper()
|
||||
oldStat, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("can't stat old file %s: %s", filename, err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if err := os.WriteFile(filename, data, perm); err != nil {
|
||||
t.Fatalf("can't write file %s: %s", filename, err)
|
||||
return
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("can't stat new file %s: %s", filename, err)
|
||||
return
|
||||
}
|
||||
|
||||
// We need different modification times.
|
||||
if !newStat.ModTime().Equal(oldStat.ModTime()) {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
37
grpc_internal.proto
Normal file
37
grpc_internal.proto
Normal file
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling";
|
||||
|
||||
package signaling;
|
||||
|
||||
service RpcInternal {
|
||||
rpc GetServerId(GetServerIdRequest) returns (GetServerIdReply) {}
|
||||
}
|
||||
|
||||
message GetServerIdRequest {
|
||||
}
|
||||
|
||||
message GetServerIdReply {
|
||||
string serverId = 1;
|
||||
}
|
41
grpc_mcu.proto
Normal file
41
grpc_mcu.proto
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling";
|
||||
|
||||
package signaling;
|
||||
|
||||
service RpcMcu {
|
||||
rpc GetPublisherId(GetPublisherIdRequest) returns (GetPublisherIdReply) {}
|
||||
}
|
||||
|
||||
message GetPublisherIdRequest {
|
||||
string sessionId = 1;
|
||||
string streamType = 2;
|
||||
}
|
||||
|
||||
message GetPublisherIdReply {
|
||||
string publisherId = 1;
|
||||
string proxyUrl = 2;
|
||||
string ip = 3;
|
||||
}
|
229
grpc_remote_client.go
Normal file
229
grpc_remote_client.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* 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 signaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
grpcRemoteClientMessageQueue = 16
|
||||
)
|
||||
|
||||
func getMD(md metadata.MD, key string) string {
|
||||
if values := md.Get(key); len(values) > 0 {
|
||||
return values[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// remoteGrpcClient is a remote client connecting from a GRPC proxy to a Hub.
|
||||
type remoteGrpcClient struct {
|
||||
hub *Hub
|
||||
client RpcSessions_ProxySessionServer
|
||||
|
||||
sessionId string
|
||||
remoteAddr string
|
||||
country string
|
||||
userAgent string
|
||||
|
||||
closeCtx context.Context
|
||||
closeFunc context.CancelCauseFunc
|
||||
|
||||
session atomic.Pointer[Session]
|
||||
messages chan WritableClientMessage
|
||||
}
|
||||
|
||||
func newRemoteGrpcClient(hub *Hub, request RpcSessions_ProxySessionServer) (*remoteGrpcClient, error) {
|
||||
md, found := metadata.FromIncomingContext(request.Context())
|
||||
if !found {
|
||||
return nil, errors.New("no metadata provided")
|
||||
}
|
||||
|
||||
closeCtx, closeFunc := context.WithCancelCause(context.Background())
|
||||
|
||||
result := &remoteGrpcClient{
|
||||
hub: hub,
|
||||
client: request,
|
||||
|
||||
sessionId: getMD(md, "sessionId"),
|
||||
remoteAddr: getMD(md, "remoteAddr"),
|
||||
country: getMD(md, "country"),
|
||||
userAgent: getMD(md, "userAgent"),
|
||||
|
||||
closeCtx: closeCtx,
|
||||
closeFunc: closeFunc,
|
||||
|
||||
messages: make(chan WritableClientMessage, grpcRemoteClientMessageQueue),
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) readPump() {
|
||||
var closeError error
|
||||
defer func() {
|
||||
c.closeFunc(closeError)
|
||||
c.hub.OnClosed(c)
|
||||
}()
|
||||
|
||||
for {
|
||||
msg, err := c.client.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// Connection was closed locally.
|
||||
break
|
||||
}
|
||||
|
||||
if status.Code(err) != codes.Canceled {
|
||||
log.Printf("Error reading from remote client for session %s: %s", c.sessionId, err)
|
||||
closeError = err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
c.hub.OnMessageReceived(c, msg.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) Context() context.Context {
|
||||
return c.client.Context()
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) RemoteAddr() string {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) UserAgent() string {
|
||||
return c.userAgent
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) Country() string {
|
||||
return c.country
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) IsConnected() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) IsAuthenticated() bool {
|
||||
return c.GetSession() != nil
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) GetSession() Session {
|
||||
session := c.session.Load()
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return *session
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) SetSession(session Session) {
|
||||
if session == nil {
|
||||
c.session.Store(nil)
|
||||
} else {
|
||||
c.session.Store(&session)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) SendError(e *Error) bool {
|
||||
message := &ServerMessage{
|
||||
Type: "error",
|
||||
Error: e,
|
||||
}
|
||||
return c.SendMessage(message)
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) SendByeResponse(message *ClientMessage) bool {
|
||||
return c.SendByeResponseWithReason(message, "")
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) SendByeResponseWithReason(message *ClientMessage, reason string) bool {
|
||||
response := &ServerMessage{
|
||||
Type: "bye",
|
||||
}
|
||||
if message != nil {
|
||||
response.Id = message.Id
|
||||
}
|
||||
if reason != "" {
|
||||
if response.Bye == nil {
|
||||
response.Bye = &ByeServerMessage{}
|
||||
}
|
||||
response.Bye.Reason = reason
|
||||
}
|
||||
return c.SendMessage(response)
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) SendMessage(message WritableClientMessage) bool {
|
||||
if c.closeCtx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
select {
|
||||
case c.messages <- message:
|
||||
return true
|
||||
default:
|
||||
log.Printf("Message queue for remote client of session %s is full, not sending %+v", c.sessionId, message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) Close() {
|
||||
c.closeFunc(nil)
|
||||
}
|
||||
|
||||
func (c *remoteGrpcClient) run() error {
|
||||
go c.readPump()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.closeCtx.Done():
|
||||
if err := context.Cause(c.closeCtx); err != context.Canceled {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case msg := <-c.messages:
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Printf("Error marshalling %+v for remote client for session %s: %s", msg, c.sessionId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.client.Send(&ServerSessionMessage{
|
||||
Message: data,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error sending %+v to remote client for session %s: %w", msg, c.sessionId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
258
grpc_server.go
Normal file
258
grpc_server.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
/**
|
||||
* 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"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
GrpcServerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGrpcServerStats()
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = newRandomString(8)
|
||||
}
|
||||
md := sha256.New()
|
||||
md.Write([]byte(fmt.Sprintf("%s-%s-%d", newRandomString(32), hostname, os.Getpid())))
|
||||
GrpcServerId = hex.EncodeToString(md.Sum(nil))
|
||||
}
|
||||
|
||||
type GrpcServerHub interface {
|
||||
GetSessionByResumeId(resumeId string) Session
|
||||
GetSessionByPublicId(sessionId string) Session
|
||||
GetSessionIdByRoomSessionId(roomSessionId string) (string, error)
|
||||
|
||||
GetBackend(u *url.URL) *Backend
|
||||
}
|
||||
|
||||
type GrpcServer struct {
|
||||
UnimplementedRpcBackendServer
|
||||
UnimplementedRpcInternalServer
|
||||
UnimplementedRpcMcuServer
|
||||
UnimplementedRpcSessionsServer
|
||||
|
||||
creds credentials.TransportCredentials
|
||||
conn *grpc.Server
|
||||
listener net.Listener
|
||||
serverId string // can be overwritten from tests
|
||||
|
||||
hub GrpcServerHub
|
||||
}
|
||||
|
||||
func NewGrpcServer(config *goconf.ConfigFile) (*GrpcServer, error) {
|
||||
var listener net.Listener
|
||||
if addr, _ := GetStringOptionWithEnv(config, "grpc", "listen"); addr != "" {
|
||||
var err error
|
||||
listener, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create GRPC listener %s: %w", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
creds, err := NewReloadableCredentials(config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := grpc.NewServer(grpc.Creds(creds))
|
||||
result := &GrpcServer{
|
||||
creds: creds,
|
||||
conn: conn,
|
||||
listener: listener,
|
||||
serverId: GrpcServerId,
|
||||
}
|
||||
RegisterRpcBackendServer(conn, result)
|
||||
RegisterRpcInternalServer(conn, result)
|
||||
RegisterRpcSessionsServer(conn, result)
|
||||
RegisterRpcMcuServer(conn, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *GrpcServer) Run() error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.conn.Serve(s.listener)
|
||||
}
|
||||
|
||||
func (s *GrpcServer) Close() {
|
||||
s.conn.GracefulStop()
|
||||
if cr, ok := s.creds.(*reloadableCredentials); ok {
|
||||
cr.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GrpcServer) LookupResumeId(ctx context.Context, request *LookupResumeIdRequest) (*LookupResumeIdReply, error) {
|
||||
statsGrpcServerCalls.WithLabelValues("LookupResumeId").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Lookup session for resume id %s", request.ResumeId)
|
||||
session := s.hub.GetSessionByResumeId(request.ResumeId)
|
||||
if session == nil {
|
||||
return nil, status.Error(codes.NotFound, "no such room session id")
|
||||
}
|
||||
|
||||
return &LookupResumeIdReply{
|
||||
SessionId: session.PublicId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GrpcServer) LookupSessionId(ctx context.Context, request *LookupSessionIdRequest) (*LookupSessionIdReply, error) {
|
||||
statsGrpcServerCalls.WithLabelValues("LookupSessionId").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Lookup session id for room session id %s", request.RoomSessionId)
|
||||
sid, err := s.hub.GetSessionIdByRoomSessionId(request.RoomSessionId)
|
||||
if errors.Is(err, ErrNoSuchRoomSession) {
|
||||
return nil, status.Error(codes.NotFound, "no such room session id")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sid != "" && request.DisconnectReason != "" {
|
||||
if session := s.hub.GetSessionByPublicId(sid); session != nil {
|
||||
log.Printf("Closing session %s because same room session %s connected", session.PublicId(), request.RoomSessionId)
|
||||
session.LeaveRoom(false)
|
||||
switch sess := session.(type) {
|
||||
case *ClientSession:
|
||||
if client := sess.GetClient(); client != nil {
|
||||
client.SendByeResponseWithReason(nil, "room_session_reconnected")
|
||||
}
|
||||
}
|
||||
session.Close()
|
||||
}
|
||||
}
|
||||
return &LookupSessionIdReply{
|
||||
SessionId: sid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GrpcServer) IsSessionInCall(ctx context.Context, request *IsSessionInCallRequest) (*IsSessionInCallReply, error) {
|
||||
statsGrpcServerCalls.WithLabelValues("IsSessionInCall").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Check if session %s is in call %s on %s", request.SessionId, request.RoomId, request.BackendUrl)
|
||||
session := s.hub.GetSessionByPublicId(request.SessionId)
|
||||
if session == nil {
|
||||
return nil, status.Error(codes.NotFound, "no such session id")
|
||||
}
|
||||
|
||||
result := &IsSessionInCallReply{}
|
||||
room := session.GetRoom()
|
||||
if room == nil || room.Id() != request.GetRoomId() || room.Backend().url != request.GetBackendUrl() ||
|
||||
(session.ClientType() != HelloClientTypeInternal && !room.IsSessionInCall(session)) {
|
||||
// Recipient is not in a room, a different room or not in the call.
|
||||
result.InCall = false
|
||||
} else {
|
||||
result.InCall = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *GrpcServer) GetPublisherId(ctx context.Context, request *GetPublisherIdRequest) (*GetPublisherIdReply, error) {
|
||||
statsGrpcServerCalls.WithLabelValues("GetPublisherId").Inc()
|
||||
// TODO: Remove debug logging
|
||||
log.Printf("Get %s publisher id for session %s", request.StreamType, request.SessionId)
|
||||
session := s.hub.GetSessionByPublicId(request.SessionId)
|
||||
if session == nil {
|
||||
return nil, status.Error(codes.NotFound, "no such session")
|
||||
}
|
||||
|
||||
clientSession, ok := session.(*ClientSession)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.NotFound, "no such session")
|
||||
}
|
||||
|
||||
publisher := clientSession.GetOrWaitForPublisher(ctx, StreamType(request.StreamType))
|
||||
if publisher, ok := publisher.(*mcuProxyPublisher); ok {
|
||||
reply := &GetPublisherIdReply{
|
||||
PublisherId: publisher.Id(),
|
||||
ProxyUrl: publisher.conn.rawUrl,
|
||||
}
|
||||
if ip := publisher.conn.ip; ip != nil {
|
||||
reply.Ip = ip.String()
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.NotFound, "no such publisher")
|
||||
}
|
||||
|
||||
func (s *GrpcServer) GetServerId(ctx context.Context, request *GetServerIdRequest) (*GetServerIdReply, error) {
|
||||
statsGrpcServerCalls.WithLabelValues("GetServerId").Inc()
|
||||
return &GetServerIdReply{
|
||||
ServerId: s.serverId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GrpcServer) GetSessionCount(ctx context.Context, request *GetSessionCountRequest) (*GetSessionCountReply, error) {
|
||||
statsGrpcServerCalls.WithLabelValues("SessionCount").Inc()
|
||||
|
||||
u, err := url.Parse(request.Url)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid url")
|
||||
}
|
||||
|
||||
backend := s.hub.GetBackend(u)
|
||||
if backend == nil {
|
||||
return nil, status.Error(codes.NotFound, "no such backend")
|
||||
}
|
||||
|
||||
return &GetSessionCountReply{
|
||||
Count: uint32(backend.Len()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GrpcServer) ProxySession(request RpcSessions_ProxySessionServer) error {
|
||||
statsGrpcServerCalls.WithLabelValues("ProxySession").Inc()
|
||||
hub, ok := s.hub.(*Hub)
|
||||
if !ok {
|
||||
return status.Error(codes.Internal, "invalid hub type")
|
||||
|
||||
}
|
||||
client, err := newRemoteGrpcClient(hub, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sid := hub.registerClient(client)
|
||||
defer hub.unregisterClient(sid)
|
||||
|
||||
return client.run()
|
||||
}
|
277
grpc_server_test.go
Normal file
277
grpc_server_test.go
Normal file
|
@ -0,0 +1,277 @@
|
|||
/**
|
||||
* 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"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dlintw/goconf"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
func (s *GrpcServer) WaitForCertificateReload(ctx context.Context) error {
|
||||
c, ok := s.creds.(*reloadableCredentials)
|
||||
if !ok {
|
||||
return errors.New("no reloadable credentials found")
|
||||
}
|
||||
|
||||
return c.WaitForCertificateReload(ctx)
|
||||
}
|
||||
|
||||
func (s *GrpcServer) WaitForCertPoolReload(ctx context.Context) error {
|
||||
c, ok := s.creds.(*reloadableCredentials)
|
||||
if !ok {
|
||||
return errors.New("no reloadable credentials found")
|
||||
}
|
||||
|
||||
return c.WaitForCertPoolReload(ctx)
|
||||
}
|
||||
|
||||
func NewGrpcServerForTestWithConfig(t *testing.T, config *goconf.ConfigFile) (server *GrpcServer, addr string) {
|
||||
for port := 50000; port < 50100; port++ {
|
||||
addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||
config.AddOption("grpc", "listen", addr)
|
||||
var err error
|
||||
server, err = NewGrpcServer(config)
|
||||
if isErrorAddressAlreadyInUse(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
t.Fatal("could not find free port")
|
||||
}
|
||||
|
||||
// Don't match with own server id by default.
|
||||
server.serverId = "dont-match"
|
||||
|
||||
go func() {
|
||||
if err := server.Run(); err != nil {
|
||||
t.Errorf("could not start GRPC server: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
return server, addr
|
||||
}
|
||||
|
||||
func NewGrpcServerForTest(t *testing.T) (server *GrpcServer, addr string) {
|
||||
config := goconf.NewConfigFile()
|
||||
return NewGrpcServerForTestWithConfig(t, config)
|
||||
}
|
||||
|
||||
func Test_GrpcServer_ReloadCerts(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
org1 := "Testing certificate"
|
||||
cert1 := GenerateSelfSignedCertificateForTesting(t, 1024, org1, key)
|
||||
|
||||
dir := t.TempDir()
|
||||
privkeyFile := path.Join(dir, "privkey.pem")
|
||||
pubkeyFile := path.Join(dir, "pubkey.pem")
|
||||
certFile := path.Join(dir, "cert.pem")
|
||||
WritePrivateKey(key, privkeyFile) // nolint
|
||||
WritePublicKey(&key.PublicKey, pubkeyFile) // nolint
|
||||
os.WriteFile(certFile, cert1, 0755) // nolint
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("grpc", "servercertificate", certFile)
|
||||
config.AddOption("grpc", "serverkey", privkeyFile)
|
||||
|
||||
UpdateCertificateCheckIntervalForTest(t, 0)
|
||||
server, addr := NewGrpcServerForTestWithConfig(t, config)
|
||||
|
||||
cp1 := x509.NewCertPool()
|
||||
if !cp1.AppendCertsFromPEM(cert1) {
|
||||
t.Fatalf("could not add certificate")
|
||||
}
|
||||
|
||||
cfg1 := &tls.Config{
|
||||
RootCAs: cp1,
|
||||
}
|
||||
conn1, err := tls.Dial("tcp", addr, cfg1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn1.Close() // nolint
|
||||
state1 := conn1.ConnectionState()
|
||||
if certs := state1.PeerCertificates; len(certs) == 0 {
|
||||
t.Errorf("expected certificates, got %+v", state1)
|
||||
} else if len(certs[0].Subject.Organization) == 0 {
|
||||
t.Errorf("expected organization, got %s", certs[0].Subject)
|
||||
} else if certs[0].Subject.Organization[0] != org1 {
|
||||
t.Errorf("expected organization %s, got %s", org1, certs[0].Subject)
|
||||
}
|
||||
|
||||
org2 := "Updated certificate"
|
||||
cert2 := GenerateSelfSignedCertificateForTesting(t, 1024, org2, key)
|
||||
replaceFile(t, certFile, cert2, 0755)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.WaitForCertificateReload(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cp2 := x509.NewCertPool()
|
||||
if !cp2.AppendCertsFromPEM(cert2) {
|
||||
t.Fatalf("could not add certificate")
|
||||
}
|
||||
|
||||
cfg2 := &tls.Config{
|
||||
RootCAs: cp2,
|
||||
}
|
||||
conn2, err := tls.Dial("tcp", addr, cfg2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn2.Close() // nolint
|
||||
state2 := conn2.ConnectionState()
|
||||
if certs := state2.PeerCertificates; len(certs) == 0 {
|
||||
t.Errorf("expected certificates, got %+v", state2)
|
||||
} else if len(certs[0].Subject.Organization) == 0 {
|
||||
t.Errorf("expected organization, got %s", certs[0].Subject)
|
||||
} else if certs[0].Subject.Organization[0] != org2 {
|
||||
t.Errorf("expected organization %s, got %s", org2, certs[0].Subject)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GrpcServer_ReloadCA(t *testing.T) {
|
||||
CatchLogForTest(t)
|
||||
serverKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serverCert := GenerateSelfSignedCertificateForTesting(t, 1024, "Server cert", serverKey)
|
||||
org1 := "Testing client"
|
||||
clientCert1 := GenerateSelfSignedCertificateForTesting(t, 1024, org1, clientKey)
|
||||
|
||||
dir := t.TempDir()
|
||||
privkeyFile := path.Join(dir, "privkey.pem")
|
||||
pubkeyFile := path.Join(dir, "pubkey.pem")
|
||||
certFile := path.Join(dir, "cert.pem")
|
||||
caFile := path.Join(dir, "ca.pem")
|
||||
WritePrivateKey(serverKey, privkeyFile) // nolint
|
||||
WritePublicKey(&serverKey.PublicKey, pubkeyFile) // nolint
|
||||
os.WriteFile(certFile, serverCert, 0755) // nolint
|
||||
os.WriteFile(caFile, clientCert1, 0755) // nolint
|
||||
|
||||
config := goconf.NewConfigFile()
|
||||
config.AddOption("grpc", "servercertificate", certFile)
|
||||
config.AddOption("grpc", "serverkey", privkeyFile)
|
||||
config.AddOption("grpc", "clientca", caFile)
|
||||
|
||||
UpdateCertificateCheckIntervalForTest(t, 0)
|
||||
server, addr := NewGrpcServerForTestWithConfig(t, config)
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(serverCert) {
|
||||
t.Fatalf("could not add certificate")
|
||||
}
|
||||
|
||||
pair1, err := tls.X509KeyPair(clientCert1, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg1 := &tls.Config{
|
||||
RootCAs: pool,
|
||||
Certificates: []tls.Certificate{pair1},
|
||||
}
|
||||
client1, err := NewGrpcClient(addr, nil, grpc.WithTransportCredentials(credentials.NewTLS(cfg1)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client1.Close() // nolint
|
||||
|
||||
ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel1()
|
||||
|
||||
if _, err := client1.GetServerId(ctx1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
org2 := "Updated client"
|
||||
clientCert2 := GenerateSelfSignedCertificateForTesting(t, 1024, org2, clientKey)
|
||||
replaceFile(t, caFile, clientCert2, 0755)
|
||||
|
||||
if err := server.WaitForCertPoolReload(ctx1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pair2, err := tls.X509KeyPair(clientCert2, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg2 := &tls.Config{
|
||||
RootCAs: pool,
|
||||
Certificates: []tls.Certificate{pair2},
|
||||
}
|
||||
client2, err := NewGrpcClient(addr, nil, grpc.WithTransportCredentials(credentials.NewTLS(cfg2)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client2.Close() // nolint
|
||||
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel2()
|
||||
|
||||
// This will fail if the CA certificate has not been reloaded by the server.
|
||||
if _, err := client2.GetServerId(ctx2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
69
grpc_sessions.proto
Normal file
69
grpc_sessions.proto
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling";
|
||||
|
||||
package signaling;
|
||||
|
||||
service RpcSessions {
|
||||
rpc LookupResumeId(LookupResumeIdRequest) returns (LookupResumeIdReply) {}
|
||||
rpc LookupSessionId(LookupSessionIdRequest) returns (LookupSessionIdReply) {}
|
||||
rpc IsSessionInCall(IsSessionInCallRequest) returns (IsSessionInCallReply) {}
|
||||
rpc ProxySession(stream ClientSessionMessage) returns (stream ServerSessionMessage) {}
|
||||
}
|
||||
|
||||
message LookupResumeIdRequest {
|
||||
string resumeId = 1;
|
||||
}
|
||||
|
||||
message LookupResumeIdReply {
|
||||
string sessionId = 1;
|
||||
}
|
||||
|
||||
message LookupSessionIdRequest {
|
||||
string roomSessionId = 1;
|
||||
// Optional: set if the session should be disconnected with a given reason.
|
||||
string disconnectReason = 2;
|
||||
}
|
||||
|
||||
message LookupSessionIdReply {
|
||||
string sessionId = 1;
|
||||
}
|
||||
|
||||
message IsSessionInCallRequest {
|
||||
string sessionId = 1;
|
||||
string roomId = 2;
|
||||
string backendUrl = 3;
|
||||
}
|
||||
|
||||
message IsSessionInCallReply {
|
||||
bool inCall = 1;
|
||||
}
|
||||
|
||||
message ClientSessionMessage {
|
||||
bytes message = 1;
|
||||
}
|
||||
|
||||
message ServerSessionMessage {
|
||||
bytes message = 1;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue