mirror of
https://github.com/ergochat/ergo.git
synced 2025-12-21 10:28:24 +01:00
Compare commits
1052 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5fb189a55 | ||
|
|
53664694c4 | ||
|
|
d26aa37f2c | ||
|
|
9ca936a777 | ||
|
|
fdd261a1e6 | ||
|
|
aef5d77b3b | ||
|
|
0ce9016098 | ||
|
|
f91d1d94f6 | ||
|
|
0119bbc36f | ||
|
|
96aa018352 | ||
|
|
68faf82787 | ||
|
|
5cda5bdac9 | ||
|
|
ed841ee62a | ||
|
|
6fdac13ad4 | ||
|
|
efc1627d23 | ||
|
|
6b8265fb17 | ||
|
|
92f069846c | ||
|
|
8913bd7fa9 | ||
|
|
064291e902 | ||
|
|
65295cbafa | ||
|
|
f0b1f34da7 | ||
|
|
f918e28513 | ||
|
|
8798676ae9 | ||
|
|
cca400de73 | ||
|
|
73e51333ad | ||
|
|
a5e435a26b | ||
|
|
17ed01c1ed | ||
|
|
8f18454e8f | ||
|
|
23844d4103 | ||
|
|
3b7db7fff7 | ||
|
|
4dcbc48159 | ||
|
|
0f5603eca2 | ||
|
|
7d4f5e4adf | ||
|
|
16568c5ab7 | ||
|
|
9a186f8e54 | ||
|
|
7828218bc7 | ||
|
|
7138e76151 | ||
|
|
e4aac56bda | ||
|
|
4da6511674 | ||
|
|
253972a9d2 | ||
|
|
a1c46a4be7 | ||
|
|
7718081440 | ||
|
|
e7501ef847 | ||
|
|
e404942d83 | ||
|
|
0a947115d6 | ||
|
|
9b9c39ddd4 | ||
|
|
e200e9fd8f | ||
|
|
66a7a488b7 | ||
|
|
28ed16261c | ||
|
|
686ce4d5b2 | ||
|
|
808799b100 | ||
|
|
e382036ddb | ||
|
|
43fe72f83e | ||
|
|
4ab1a10eec | ||
|
|
54b17b0700 | ||
|
|
2cf569c5d9 | ||
|
|
a4194c38d8 | ||
|
|
5bab190d33 | ||
|
|
68cee9e2cd | ||
|
|
9c3173f573 | ||
|
|
98e04c10a8 | ||
|
|
a6df370bd9 | ||
|
|
9791606f62 | ||
|
|
7256d83ff0 | ||
|
|
f5bb5afdd6 | ||
|
|
d3eb787a1e | ||
|
|
19dbe10c99 | ||
|
|
467df24914 | ||
|
|
9dc2fd52ed | ||
|
|
a46732f6ab | ||
|
|
ea81ec86e1 | ||
|
|
4bcd008416 | ||
|
|
aed216a62e | ||
|
|
f3e24c7bdb | ||
|
|
23b65e225b | ||
|
|
4ced4ef328 | ||
|
|
ec3417be79 | ||
|
|
7e18362d35 | ||
|
|
eb84ede5f7 | ||
|
|
d50f1471eb | ||
|
|
d9f663c400 | ||
|
|
e1b5a05c27 | ||
|
|
a850602bcc | ||
|
|
d1126b53eb | ||
|
|
4851825d4f | ||
|
|
8fa6e19c2e | ||
|
|
07669f9eb4 | ||
|
|
4dfb7cc7ae | ||
|
|
b6a8cc20c2 | ||
|
|
cf7db4bc2a | ||
|
|
b6f6959acc | ||
|
|
af124cd964 | ||
|
|
e60afda556 | ||
|
|
c92f23b0cb | ||
|
|
656eea43e7 | ||
|
|
881f403164 | ||
|
|
b38ca31ced | ||
|
|
7b71839615 | ||
|
|
9dd7a2bbcb | ||
|
|
148d743eb1 | ||
|
|
2a79f64f2d | ||
|
|
799e1b14f4 | ||
|
|
2163d96348 | ||
|
|
e520ba7e0e | ||
|
|
92e2aa987e | ||
|
|
ab2d842b27 | ||
|
|
21ee867ebb | ||
|
|
36e5451aa5 | ||
|
|
efd3764337 | ||
|
|
375079e636 | ||
|
|
38862b0529 | ||
|
|
2bb9980e56 | ||
|
|
1bdc45ebb4 | ||
|
|
eddd4cc723 | ||
|
|
726d997d07 | ||
|
|
9577e87d9a | ||
|
|
7586520032 | ||
|
|
f68d32b4ee | ||
|
|
796bc198ed | ||
|
|
df6aa4c34b | ||
|
|
30f47a9b22 | ||
|
|
92a23229f8 | ||
|
|
825b4298b8 | ||
|
|
eba6d532ea | ||
|
|
7d3971835e | ||
|
|
99393d49bf | ||
|
|
82c50cc497 | ||
|
|
ce41f501c9 | ||
|
|
d25fc2a758 | ||
|
|
f598da300d | ||
|
|
bb6c7ee158 | ||
|
|
958eb43393 | ||
|
|
9b8562c211 | ||
|
|
2bb0a9c8e3 | ||
|
|
0b333c7e72 | ||
|
|
2aec5e167c | ||
|
|
3127353b84 | ||
|
|
654381071b | ||
|
|
71671405f3 | ||
|
|
aa6be594b9 | ||
|
|
6326982767 | ||
|
|
0517b5571d | ||
|
|
1117680fdd | ||
|
|
f44d902ce3 | ||
|
|
7318e48629 | ||
|
|
60f7d1122d | ||
|
|
289b78d2fd | ||
|
|
ad0149be5e | ||
|
|
d81494ac09 | ||
|
|
54ca659e57 | ||
|
|
794b4a2483 | ||
|
|
af521c844f | ||
|
|
7772b55cab | ||
|
|
ed683bff79 | ||
|
|
5ee32cda1c | ||
|
|
218f6f2454 | ||
|
|
ca4b9c15c5 | ||
|
|
6abb291290 | ||
|
|
ccc362be84 | ||
|
|
19b9867409 | ||
|
|
f6626ddb6e | ||
|
|
40ceb4956c | ||
|
|
74fa04c5ea | ||
|
|
15d686c593 | ||
|
|
f96f918ff1 | ||
|
|
7726160ec7 | ||
|
|
b426dd8f93 | ||
|
|
1f4b5248a0 | ||
|
|
0c804f8ea3 | ||
|
|
3d2f014d4c | ||
|
|
d56e4ea301 | ||
|
|
8d082865da | ||
|
|
837f6ac1a2 | ||
|
|
681e8b1292 | ||
|
|
432d4ea860 | ||
|
|
78f342655d | ||
|
|
cab192e2af | ||
|
|
c67835ce5c | ||
|
|
7afd6dbc74 | ||
|
|
ee7f818674 | ||
|
|
8475b62da4 | ||
|
|
52d15a483c | ||
|
|
f691b8c058 | ||
|
|
6b7bfe0c09 | ||
|
|
2098cc9f2b | ||
|
|
4b9aa725cb | ||
|
|
24ac3b68b4 | ||
|
|
0918564edc | ||
|
|
921651f664 | ||
|
|
d97e964b35 | ||
|
|
010875ec9a | ||
|
|
7b525f8899 | ||
|
|
3839f8ae60 | ||
|
|
4e574b99f3 | ||
|
|
9d388d8cdb | ||
|
|
24cf5fac45 | ||
|
|
d238eaac67 | ||
|
|
0f059ea2cc | ||
|
|
dfe2a21b17 | ||
|
|
1d8bbde95c | ||
|
|
580fc7096d | ||
|
|
15c074078a | ||
|
|
4aa1aa371d | ||
|
|
a4d160b76d | ||
|
|
430387dec6 | ||
|
|
ce162e9279 | ||
|
|
97d6f9eddb | ||
|
|
6be1ec3ad6 | ||
|
|
16ab0a67b5 | ||
|
|
cc1c491afe | ||
|
|
8d80cb52e6 | ||
|
|
e11bda643e | ||
|
|
b1a0e7cc5c | ||
|
|
2d44ab1cbf | ||
|
|
3102babec8 | ||
|
|
a5af245102 | ||
|
|
4fabeed895 | ||
|
|
5671ee2a36 | ||
|
|
4d9e80fe5b | ||
|
|
7b3778989e | ||
|
|
e3bcb9b8a0 | ||
|
|
70dfe9f594 | ||
|
|
70a98ac2f1 | ||
|
|
046ef8ce94 | ||
|
|
baf5a8465d | ||
|
|
b33e1051f7 | ||
|
|
ddb804b622 | ||
|
|
3ec7f0e5cc | ||
|
|
48d139a532 | ||
|
|
556bcba465 | ||
|
|
20bfb285f0 | ||
|
|
29b4be83bc | ||
|
|
399b0b3f39 | ||
|
|
e7597876d9 | ||
|
|
3bd3c6a88a | ||
|
|
2013beb7c8 | ||
|
|
6b386ce2ac | ||
|
|
ee22bda09c | ||
|
|
202de687df | ||
|
|
4b00c6c48e | ||
|
|
8ac488a1ff | ||
|
|
f07707dfbc | ||
|
|
3b3e8c0004 | ||
|
|
f77d430d25 | ||
|
|
28d9a7ff63 | ||
|
|
b3abd0bf1d | ||
|
|
cc873efd0f | ||
|
|
3f74612e2b | ||
|
|
24ba72cfd6 | ||
|
|
17b21c8521 | ||
|
|
75bd63d0bc | ||
|
|
3c4f83cf6e | ||
| 67d10bc63b | |||
|
|
6d642bfe93 | ||
|
|
ad3ad97047 | ||
|
|
d14ff9b3d5 | ||
|
|
dfe84bc1c2 | ||
|
|
0f39fde647 | ||
|
|
7d6e48ed2a | ||
|
|
e4c8f041f2 | ||
|
|
783b579003 | ||
|
|
07cc4f8354 | ||
|
|
f100c1d0fa | ||
|
|
2aded271c5 | ||
|
|
3d4d8228aa | ||
|
|
60af8ee491 | ||
|
|
38a6d17ee5 | ||
|
|
d082ec7ab9 | ||
|
|
3e68694760 | ||
|
|
48f8c341d7 | ||
|
|
00cfe98461 | ||
|
|
bf33fba33a | ||
|
|
0710c7e12a | ||
|
|
e84793d7ee | ||
|
|
2c0928f94d | ||
|
|
0d8dcbecf6 | ||
|
|
eeec481b8d | ||
|
|
378d88fee2 | ||
|
|
c4db4984a6 | ||
|
|
04f8791dd6 | ||
|
|
37eb5f5804 | ||
|
|
6e011cd536 | ||
|
|
295a567eda | ||
|
|
db0910d82d | ||
|
|
374cf8ef97 | ||
|
|
eb83df420b | ||
|
|
3fca52ba38 | ||
|
|
3d1412a898 | ||
|
|
b155e5315b | ||
|
|
7c53b9430a | ||
|
|
3c59ce964d | ||
|
|
ae04fb3d0a | ||
|
|
ba40d57afd | ||
|
|
697f34995b | ||
|
|
19dbf3a531 | ||
|
|
8b6b2cabc3 | ||
|
|
1da11ae8ae | ||
|
|
12f7796933 | ||
|
|
fc89d72045 | ||
| 0653f90b4f | |||
|
|
abb38ce8a1 | ||
|
|
5ecf19d01e | ||
|
|
abc71684f3 | ||
|
|
9439e9b9e1 | ||
|
|
5eaf7b37e5 | ||
|
|
4317016a09 | ||
|
|
7193fa3a3c | ||
|
|
cd36604efe | ||
|
|
8199edee6c | ||
|
|
81832a26bc | ||
|
|
8690a7648b | ||
|
|
7e6c658cad | ||
|
|
eb84103865 | ||
|
|
7a82554f9d | ||
|
|
05e5fe3444 | ||
|
|
3f5de80afd | ||
|
|
b2087977d0 | ||
|
|
177133a96f | ||
|
|
b16350e559 | ||
|
|
16e214e4fb | ||
|
|
46d32520c7 | ||
|
|
f72a6fa011 | ||
|
|
1e6dee15b2 | ||
|
|
3ceff6a8b1 | ||
|
|
7ce0636276 | ||
|
|
bceae9b739 | ||
|
|
30fbfe4cc0 | ||
|
|
2a828bb783 | ||
|
|
4b3a6cb611 | ||
|
|
f00fd452be | ||
|
|
f6f7315458 | ||
|
|
1e1acdae21 | ||
|
|
df8eef5b0a | ||
|
|
23ba58b327 | ||
|
|
bf4f3008d4 | ||
|
|
63c08ce537 | ||
|
|
f7ab0fb59e | ||
|
|
d0c01301fd | ||
|
|
a052b82c78 | ||
|
|
238407c70e | ||
|
|
9ab8b6710c | ||
|
|
05e5e88de4 | ||
|
|
d17faf6bcb | ||
|
|
77de026961 | ||
|
|
898f84c613 | ||
|
|
ae1de2554e | ||
|
|
893922afe0 | ||
|
|
99d27ff737 | ||
|
|
fa3de3e149 | ||
|
|
2bfa13b7d0 | ||
|
|
25e698d57f | ||
|
|
30b760483e | ||
|
|
825cdab67d | ||
|
|
f665525735 | ||
|
|
35b5613349 | ||
|
|
a5983a1bd1 | ||
|
|
ebda5e6d9a | ||
|
|
e40f550af8 | ||
|
|
e20c983b57 | ||
|
|
c3b3bf9941 | ||
|
|
dd8073208c | ||
|
|
062491ebfc | ||
|
|
7df041d0a6 | ||
|
|
06a204d0d3 | ||
|
|
a9c77af1cb | ||
|
|
4e0d2d65e8 | ||
|
|
57a213123f | ||
|
|
746309e386 | ||
|
|
531a1d6864 | ||
|
|
0e8f447326 | ||
|
|
7ad31497c2 | ||
|
|
7d6ff58bf8 | ||
|
|
68bd2d87e0 | ||
|
|
8ff5a048f3 | ||
|
|
594991d6cc | ||
| df234b842e | |||
|
|
35128bfc23 | ||
|
|
507dc2d838 | ||
|
|
a99c8a42f9 | ||
|
|
de1be675f5 | ||
|
|
5b72cd8622 | ||
|
|
096c12fb52 | ||
|
|
1d10eb934a | ||
|
|
26e0dae11d | ||
|
|
321ff109b1 | ||
|
|
86f124e938 | ||
|
|
2138847984 | ||
|
|
f032fda48d | ||
|
|
2cace0b5a2 | ||
|
|
4208e11571 | ||
|
|
c0e7aac862 | ||
|
|
69448b13a1 | ||
|
|
810ec75f95 | ||
|
|
86f7668c68 | ||
|
|
e8cd87d8fd | ||
|
|
101fd53d6d | ||
|
|
acd95b9924 | ||
|
|
40d70b8aeb | ||
|
|
ef088373a8 | ||
|
|
ae55a4c660 | ||
|
|
2b86660e5c | ||
|
|
c3d4be45f1 | ||
|
|
67b2f4ccd2 | ||
|
|
104d0321e8 | ||
|
|
14d1614bba | ||
|
|
e48c3fa687 | ||
| 16b8d9090b | |||
|
|
5e5cc3040b | ||
|
|
bcaed1aff1 | ||
|
|
7192df4592 | ||
|
|
da07c0072c | ||
|
|
6f9e07d2a2 | ||
|
|
52e0f8e7e9 | ||
|
|
d6d5bbe27b | ||
|
|
737697d1d4 | ||
|
|
dd75eb1084 | ||
|
|
a13235880c | ||
|
|
87789676c0 | ||
|
|
c454c45d6a | ||
|
|
4948b48b8f | ||
|
|
c5579a6a34 | ||
|
|
b11dc1c84c | ||
|
|
78548aa9df | ||
|
|
03092769e7 | ||
|
|
d5814c10ab | ||
|
|
34ad3a2dc1 | ||
|
|
f7853b15ca | ||
|
|
077081076c | ||
|
|
dea2e7961a | ||
|
|
c603d41d08 | ||
|
|
c87dead39b | ||
|
|
66bf6244f3 | ||
| e6905f4543 | |||
|
|
71fe4ecf48 | ||
|
|
8eaf6f5166 | ||
|
|
2df5fb1956 | ||
|
|
42883972a8 | ||
|
|
2a3b8e648c | ||
|
|
ae5e1fb49f | ||
|
|
15303d0247 | ||
|
|
432f0f62d5 | ||
|
|
374bd834fd | ||
|
|
5ecba1d40b | ||
|
|
5c7df07d91 | ||
|
|
911b00787b | ||
|
|
2b8eb93c00 | ||
|
|
51cdebf167 | ||
|
|
61fd7a2534 | ||
|
|
7201f14b8b | ||
|
|
504cc44bf7 | ||
|
|
379632a9e6 | ||
|
|
1f08c97238 | ||
|
|
2c488f5ebf | ||
|
|
2fb8b836db | ||
|
|
ac2fc0da28 | ||
|
|
934ad1cec2 | ||
|
|
1adda8d42c | ||
|
|
32f7868bfd | ||
|
|
6bd94391ef | ||
|
|
b7e2bd9f33 | ||
|
|
fd5317e68e | ||
|
|
a549827f17 | ||
|
|
446c654dea | ||
|
|
6d892fe371 | ||
|
|
9f6e26450b | ||
|
|
4010f3fc02 | ||
|
|
99294b8968 | ||
|
|
ba474b9b9a | ||
|
|
302c9cb908 | ||
|
|
87d9addcfc | ||
|
|
7c766b2096 | ||
|
|
b66ea9f56d | ||
|
|
1e7775f6de | ||
|
|
197a9d4b5e | ||
|
|
cba3a2fc10 | ||
|
|
2b0d94dfee | ||
|
|
0a2a850005 | ||
|
|
e7abd93e90 | ||
|
|
0afa7edffe | ||
|
|
59ef59870a | ||
|
|
b492b8385b | ||
|
|
fcb86c54f7 | ||
|
|
6c0e9619cb | ||
|
|
e3e8136f85 | ||
|
|
74f3ea1d2e | ||
|
|
fa7b76d66a | ||
|
|
e3c9eb8e71 | ||
|
|
c2bf59ca38 | ||
|
|
86c5839044 | ||
|
|
eb477c3793 | ||
|
|
bf3c0ad70e | ||
|
|
0da7e68e6d | ||
|
|
6dc6abc455 | ||
|
|
a73ad5fc10 | ||
|
|
1d8a54289f | ||
|
|
d8c9d0ec03 | ||
|
|
2e26efa55d | ||
|
|
7347e94da1 | ||
|
|
3162c8a1c8 | ||
|
|
8605cd2295 | ||
|
|
28ab3612e2 | ||
|
|
dba5d3faae | ||
|
|
1953e90720 | ||
|
|
e6951aca3f | ||
|
|
2619a23458 | ||
|
|
a0ad42272d | ||
|
|
7a6e7f05a1 | ||
|
|
6863d58cab | ||
|
|
d68813927a | ||
|
|
c2a5853d08 | ||
|
|
d174b5aad6 | ||
|
|
fb2be58e60 | ||
|
|
abd4cf7d3b | ||
|
|
2dde9cb464 | ||
|
|
c01e686221 | ||
|
|
0a59f41cf9 | ||
|
|
58d8421f44 | ||
|
|
378f9cc852 | ||
|
|
d6b8e59462 | ||
|
|
40bd298a91 | ||
|
|
ed75533cb1 | ||
|
|
e112a78b9b | ||
|
|
a57bf46e6a | ||
|
|
b929691470 | ||
|
|
24ad24562e | ||
|
|
000eb760e6 | ||
|
|
d84d6756ed | ||
|
|
f5598cfc1c | ||
|
|
8b74cd1fd3 | ||
|
|
e15c355f18 | ||
|
|
5b3ec9a605 | ||
|
|
76f7748c8a | ||
|
|
c5746c5105 | ||
|
|
ec4fb90d2b | ||
|
|
aa4a82e0c8 | ||
|
|
8fc20d8eed | ||
|
|
98e87f6cc0 | ||
|
|
9293858ba1 | ||
| 15f5f2e9b0 | |||
|
|
0483e3f6ad | ||
|
|
f3f805acb8 | ||
|
|
1cd31f4d61 | ||
|
|
7bc5bfaa5c | ||
|
|
4f7356f19a | ||
|
|
0d0d9e72b4 | ||
|
|
ac17bf0e9d | ||
|
|
e5dff58647 | ||
|
|
9d9ee11224 | ||
|
|
3fe8d01d50 | ||
|
|
8be8f0f08d | ||
|
|
f40d868cf5 | ||
|
|
3e32e3f19e | ||
|
|
4d6e0120b2 | ||
|
|
5c7f8faf0c | ||
|
|
039d8f3f2d | ||
|
|
b8e38819d8 | ||
|
|
fba41a26df | ||
|
|
1951e09eea | ||
|
|
cb757c703d | ||
|
|
72959eb1cf | ||
|
|
8ec9053448 | ||
|
|
7d66368274 | ||
|
|
028f2fcaa4 | ||
|
|
645721f97e | ||
|
|
8995dd8842 | ||
|
|
fd45529d94 | ||
|
|
eef9753912 | ||
|
|
9ff4047fa6 | ||
|
|
ee720f60e2 | ||
|
|
c51569420a | ||
|
|
20270aeb22 | ||
|
|
57c943c812 | ||
|
|
0344e99bc6 | ||
|
|
72b51c00de | ||
| b106c01303 | |||
|
|
5c7fd0ec0b | ||
|
|
14c16a999d | ||
| e0f4fcee61 | |||
|
|
ac91beabfd | ||
|
|
21e637b8d7 | ||
|
|
208a6706c5 | ||
|
|
43b0e020e8 | ||
|
|
86e5f907ea | ||
|
|
bc5c2a1250 | ||
|
|
8218d1caab | ||
|
|
51fca3cc0b | ||
|
|
3b3c5b591f | ||
|
|
0713d040be | ||
|
|
2038763e10 | ||
|
|
62b2d0341e | ||
|
|
250a95c8e5 | ||
|
|
9ceac66b08 | ||
|
|
92f6bf2d03 | ||
|
|
c70e518eed | ||
|
|
54b1513931 | ||
|
|
9a007d07f8 | ||
|
|
7ee0712401 | ||
|
|
050e27b31b | ||
|
|
e74da6c51e | ||
|
|
741cd8e8af | ||
|
|
3d4170ef98 | ||
|
|
4bffdba610 | ||
|
|
4a3ac617a5 | ||
|
|
dc75b24d23 | ||
|
|
9f0c3cdc0e | ||
|
|
2274ef3fa0 | ||
|
|
63826da693 | ||
|
|
e154126ac2 | ||
| 9d9ae8ce14 | |||
|
|
486bd699eb | ||
|
|
ea4b93dd59 | ||
|
|
4901e67f51 | ||
|
|
b276d23230 | ||
|
|
0f8f8b0de9 | ||
|
|
f9ca172ad7 | ||
|
|
9c4fbeabef | ||
|
|
475814d613 | ||
|
|
e620e3b4bf | ||
|
|
7e93770540 | ||
|
|
d488cf7f57 | ||
|
|
ad8c97c9bb | ||
|
|
c4e376c8bb | ||
|
|
85fabaad6d | ||
|
|
4ba35afa85 | ||
|
|
759696836e | ||
|
|
1ab2e9c294 | ||
|
|
c9b54ee2b8 | ||
|
|
ea1678ec8f | ||
|
|
62043afbb0 | ||
|
|
61bce74018 | ||
|
|
51d573d3c9 | ||
|
|
8c556fe8c5 | ||
|
|
4749d7e776 | ||
|
|
3ec5ffa340 | ||
|
|
84a5b83eb1 | ||
|
|
48897596c4 | ||
|
|
41089b0e16 | ||
|
|
84fef29760 | ||
|
|
5bbee02fe6 | ||
|
|
b478e93c11 | ||
|
|
c972a92e51 | ||
|
|
7d5cb723b4 | ||
|
|
404bf6c2a0 | ||
|
|
1b55520006 | ||
|
|
53a7e8c334 | ||
|
|
20d8d269ca | ||
|
|
b0f412538c | ||
|
|
3cf1583aed | ||
|
|
2cae19dde5 | ||
|
|
284c3d689b | ||
|
|
e0e4791f72 | ||
|
|
657ce0f1a4 | ||
|
|
fc711de360 | ||
|
|
e71643eb73 | ||
|
|
2b3fc9d38e | ||
|
|
b8009c4a07 | ||
|
|
bce3d643bc | ||
|
|
c7b6b6e917 | ||
|
|
eff6dd242b | ||
|
|
cb39c82222 | ||
|
|
69d88fb231 | ||
|
|
b83479247e | ||
|
|
39b0d2c6ac | ||
|
|
a367c20410 | ||
|
|
d02eecd8ec | ||
|
|
ffb5e4f986 | ||
|
|
aa969b7ff7 | ||
|
|
585910a9b1 | ||
|
|
9f7ead07a5 | ||
|
|
9b6ec04ca5 | ||
|
|
55cf1e6781 | ||
|
|
26cdb4cf36 | ||
|
|
8b2f6de3e0 | ||
|
|
0baaf0b711 | ||
|
|
a1d4b8ac82 | ||
|
|
12947644e2 | ||
|
|
9c77f89bc2 | ||
|
|
492109f29d | ||
|
|
00255586cc | ||
|
|
fea8cc1b9a | ||
|
|
a90fbf9f2c | ||
|
|
0d438dd0d6 | ||
|
|
f33f41b0eb | ||
|
|
4785a3953a | ||
|
|
f6f25039b7 | ||
|
|
5d2d4a99bc | ||
|
|
abfb8442ab | ||
|
|
bbb52bf692 | ||
|
|
e894c44960 | ||
|
|
ada135d7cf | ||
|
|
0355c2df1e | ||
|
|
37c7b97084 | ||
|
|
0ac8b6daea | ||
|
|
c3fb7f2ad6 | ||
|
|
1c5a485c17 | ||
|
|
5b93fdfcf2 | ||
|
|
ecd878c169 | ||
|
|
117401f293 | ||
|
|
b13776787b | ||
|
|
c5a9916302 | ||
|
|
1389d89a9b | ||
|
|
941c12244f | ||
|
|
cf25e894e1 | ||
|
|
4dd9af8f06 | ||
|
|
d8dc24dee8 | ||
|
|
ebe1f84d64 | ||
|
|
41822813c0 | ||
|
|
e1401934df | ||
|
|
3264687803 | ||
|
|
ef92318282 | ||
|
|
7fde04ea94 | ||
|
|
ad61f9f213 | ||
|
|
6851901e20 | ||
|
|
c99b2be403 | ||
|
|
699921afff | ||
| 99610eae4b | |||
|
|
128142ca41 | ||
|
|
dc0bf1a02d | ||
|
|
5b317d4846 | ||
|
|
f58f8531b2 | ||
|
|
54c5d35193 | ||
|
|
907f82a27e | ||
|
|
497aa429b7 | ||
|
|
7190770e12 | ||
|
|
3fde046a01 | ||
|
|
29f1afd565 | ||
| b2ea2583f4 | |||
|
|
4693a88421 | ||
|
|
5d0e4fa023 | ||
|
|
59bddd066f | ||
|
|
032ca175e4 | ||
|
|
46572b871f | ||
|
|
1c89f996bc | ||
|
|
dcfd8d8fe8 | ||
|
|
fedf4a9176 | ||
|
|
98c4d0e399 | ||
|
|
5fc7ac41da | ||
|
|
5e5c86ad86 | ||
|
|
364193df4e | ||
|
|
dbfa704eb2 | ||
|
|
6f24082705 | ||
|
|
c53df2dc88 | ||
|
|
188d8c499d | ||
|
|
77bfdd8619 | ||
|
|
6e72f12992 | ||
|
|
d0801e45a8 | ||
| 99cb1fd02c | |||
|
|
62d78a64cb | ||
|
|
9adc77498e | ||
|
|
1121c71d75 | ||
|
|
0751f31b9e | ||
|
|
5daabdd226 | ||
|
|
ff3f959d52 | ||
|
|
9af6b86868 | ||
|
|
0a811f9d9e | ||
|
|
b68696eb9b | ||
|
|
20aa8efe56 | ||
|
|
e8839407d4 | ||
|
|
f07524111c | ||
|
|
234459a2fe | ||
|
|
e60c2a6806 | ||
|
|
2abfa66802 | ||
|
|
b6264a43b6 | ||
|
|
51c207dc80 | ||
|
|
e5c2588eab | ||
|
|
6786b87fbd | ||
|
|
d03615d29a | ||
|
|
99b9312847 | ||
|
|
21f51dcc3e | ||
|
|
4910aefa37 | ||
|
|
66af8cd63c | ||
|
|
d097d34737 | ||
|
|
8f8b71761d | ||
| 8efb2d23c3 | |||
|
|
ece82b44fb | ||
|
|
29982e3ffe | ||
|
|
57c746bd57 | ||
| ca99a65dc4 | |||
|
|
c53097000b | ||
|
|
2f5484a673 | ||
|
|
725bb7213d | ||
|
|
e59d599eed | ||
|
|
94afd012fb | ||
|
|
5d4a12f008 | ||
|
|
97d1786378 | ||
|
|
0898a6aa91 | ||
|
|
fcaefaca9c | ||
|
|
9851d2e9bc | ||
|
|
6cfd8eadc9 | ||
|
|
1d832ee1bc | ||
|
|
36703580fc | ||
|
|
99a48496fa | ||
|
|
b81757d273 | ||
|
|
1b894b73a2 | ||
|
|
f93e1f1a7d | ||
|
|
b9a1cd618e | ||
|
|
6b9cdfeed9 | ||
|
|
33e3b0ce1b | ||
|
|
612f527033 | ||
|
|
6ff0486aa0 | ||
|
|
75208d2934 | ||
|
|
c24254fe45 | ||
|
|
81b5fa865f | ||
|
|
ac806e5c62 | ||
|
|
202d982866 | ||
|
|
76fa365a7a | ||
|
|
cf33122f15 | ||
|
|
d85fe8c3cc | ||
|
|
c3592274dc | ||
|
|
351eb8ad27 | ||
|
|
ec48966b68 | ||
|
|
91cdb96bcb | ||
|
|
98ea150817 | ||
|
|
77313e20ad | ||
|
|
9527850f7c | ||
|
|
b0bdbb775c | ||
|
|
e76b14d036 | ||
|
|
d740a161db | ||
|
|
b5a154d3d1 | ||
|
|
60d351f9ff | ||
|
|
d6b8cb9a8d | ||
|
|
7944871eb6 | ||
|
|
67868f85e8 | ||
|
|
9dad717c04 | ||
|
|
42296bdc49 | ||
|
|
f1ae8051cb | ||
|
|
4d4e134008 | ||
|
|
23c7218bf1 | ||
|
|
7a1695c628 | ||
|
|
a300524458 | ||
|
|
588efd29b4 | ||
|
|
cc4b958a41 | ||
|
|
7c5a8f2013 | ||
|
|
297f2af827 | ||
|
|
56a0407ff5 | ||
|
|
ba21987d03 | ||
|
|
df49137aca | ||
|
|
a131507090 | ||
|
|
503e575633 | ||
|
|
b0b4f0492c | ||
|
|
14cc20c2a0 | ||
|
|
d35f38f161 | ||
|
|
648ad4a4c3 | ||
|
|
9100fb7ca1 | ||
|
|
9983478479 | ||
|
|
23ffc6ef91 | ||
|
|
a9294e628f | ||
|
|
8b693ec24f | ||
|
|
33616df7c4 | ||
|
|
aa27ad98a8 | ||
|
|
5c157adf45 | ||
|
|
2d31a16647 | ||
|
|
317720bfc8 | ||
|
|
e14aace1da | ||
|
|
973d7dc1dc | ||
|
|
97ba1c3d63 | ||
|
|
75f89a9f1f | ||
|
|
5eed48c077 | ||
|
|
a2b5548c8b | ||
|
|
c53926acde | ||
|
|
0b414cb158 | ||
|
|
6b8a487b0c | ||
|
|
8bf6231ec6 | ||
|
|
4700d4c048 | ||
|
|
7345ecba48 | ||
|
|
76697dff0f | ||
|
|
c74a64b888 | ||
|
|
0a1537f928 | ||
|
|
88f8caad0b | ||
|
|
0d05ab4ff4 | ||
|
|
50d32924ef | ||
|
|
da216fc699 | ||
|
|
d2278faf75 | ||
|
|
a7db4a669e | ||
|
|
6f56121662 | ||
|
|
c62edcc909 | ||
|
|
57c5030e91 | ||
|
|
3ceb346c61 | ||
|
|
3e05502c3f | ||
|
|
517b776b62 | ||
|
|
1a5d079670 | ||
|
|
eb2dfa78c9 | ||
|
|
fed002d11a | ||
|
|
3cca1e2c39 | ||
|
|
fed5134a63 | ||
|
|
7481bf0385 | ||
|
|
41738471ce | ||
|
|
319b9a6c6e | ||
|
|
022330b9b8 | ||
|
|
cda268fe1e | ||
|
|
197d487f40 | ||
|
|
2a99a8e2d5 | ||
|
|
48932e7ab1 | ||
|
|
a0a4ab4e17 | ||
|
|
8dd12b0693 | ||
|
|
639817f014 | ||
|
|
5cd76f89d4 | ||
|
|
6817186224 | ||
|
|
745fd764dd | ||
|
|
db41b2bc34 | ||
|
|
1fc513cef0 | ||
|
|
aecb28a616 | ||
|
|
f9c1a00b91 | ||
|
|
2e3e4f72ba | ||
|
|
f0796b2eb5 | ||
|
|
b83b051632 | ||
|
|
5b33cd436f | ||
|
|
549d06bc98 | ||
|
|
18b6e2f1cd | ||
|
|
4052cd12fe | ||
|
|
2e9a0d4b2d | ||
|
|
fd3cbab6ee | ||
|
|
b022c34a23 | ||
|
|
0b5544831d | ||
|
|
1f2f740344 | ||
|
|
46c32094d7 | ||
|
|
ab870c2ffe | ||
|
|
67ee36f1ed | ||
|
|
a6cf667f06 | ||
|
|
074a5a077e | ||
|
|
e447c61c73 | ||
|
|
cd43fae478 | ||
|
|
0db7f88637 | ||
|
|
07edf2dc1f | ||
|
|
70b20750aa | ||
|
|
88b877fce4 | ||
|
|
1efde964e1 | ||
|
|
507d53c507 | ||
|
|
fe8e6551c3 | ||
|
|
76b0e44474 | ||
|
|
5c4984f45f | ||
|
|
44ed0b7a38 | ||
|
|
4e49a25ba6 | ||
|
|
de31430fdc | ||
|
|
131eb229bc | ||
|
|
896ed91d45 | ||
|
|
19642680d2 | ||
|
|
f495d30f1d | ||
|
|
6e20e44879 | ||
|
|
c1e9ac4c3c | ||
|
|
788b37b12a | ||
|
|
03185ea4a9 | ||
|
|
6fae02d335 | ||
|
|
f05c57344e | ||
|
|
7b8e15ff1d | ||
|
|
26cb622f92 | ||
|
|
ad3306fa3b | ||
|
|
780bdc4cd2 | ||
|
|
9c7e98ab53 | ||
|
|
23164054f6 | ||
| da84082e0a | |||
|
|
1762a168e8 | ||
|
|
00edd78f59 | ||
|
|
088f4d1a37 | ||
|
|
22af40c995 | ||
|
|
72f41f6b3f | ||
|
|
96f575c739 | ||
|
|
872ca646f3 | ||
|
|
992a09dc90 | ||
|
|
eeb5f9b24d | ||
|
|
143e6ba9e3 | ||
|
|
d7ba478519 | ||
|
|
67db9f3564 | ||
|
|
3a62fde6bd | ||
|
|
1642121203 | ||
|
|
a3e5b3d991 | ||
|
|
8180c2b572 | ||
|
|
66f1ea9fd4 | ||
|
|
19a71849cf | ||
|
|
1d3d9f4cf5 | ||
|
|
16f0170512 | ||
|
|
e9d42e02a2 | ||
|
|
29666107ab | ||
|
|
d547d05205 | ||
|
|
640572e151 | ||
|
|
cbaa6af9bd | ||
|
|
a6f3e2c748 | ||
|
|
2681097516 | ||
|
|
cd5b6211b3 | ||
|
|
092f193326 | ||
|
|
7af94c79fd | ||
|
|
08eabea78f | ||
|
|
1f3f9f18d9 | ||
|
|
56bef19505 | ||
|
|
9e25a3027a | ||
|
|
3fc277e733 | ||
|
|
7814694d17 | ||
|
|
430b40fc2f | ||
|
|
dc4214a8ca | ||
|
|
8de7a0edbe | ||
|
|
58e3694e84 | ||
|
|
82eb55e0b2 | ||
|
|
a6545728bd | ||
|
|
e957a89ee2 | ||
|
|
2fbbfb1337 | ||
|
|
4d3f9dd509 | ||
|
|
43cc2bf9be | ||
|
|
d92bcf0358 | ||
|
|
887fda5221 | ||
|
|
1fb736b394 | ||
|
|
155fb3ea21 | ||
|
|
0aa725e9f2 | ||
|
|
d90124ca42 | ||
|
|
819228fdb5 | ||
|
|
3f6971e087 | ||
|
|
bb4a129c9a | ||
|
|
6c727dfc1c | ||
|
|
e0d3b54aaf | ||
|
|
3a1327966b | ||
|
|
aef082fd71 | ||
|
|
a164b7aef9 | ||
|
|
7969229a07 | ||
|
|
c0fc2620e3 | ||
|
|
3a8e9ec2ff | ||
|
|
41fd2cbabe | ||
|
|
7d08b83eff | ||
|
|
014d141b88 | ||
|
|
e95ddc72af | ||
|
|
4da60a41d7 | ||
|
|
f1c927bbf8 | ||
|
|
f71345d096 | ||
|
|
64b868a725 | ||
|
|
d873426f5b | ||
|
|
06e109e347 | ||
|
|
528678e2c9 | ||
|
|
38275601e1 | ||
|
|
fdfa9de79f | ||
|
|
8876b61b9c | ||
|
|
230fdc57c8 | ||
|
|
0956ce78f4 | ||
|
|
cf5d6bdf9a | ||
|
|
14ff84a504 | ||
|
|
6954d04af9 | ||
|
|
5ad165e31e | ||
|
|
9fd4210416 | ||
|
|
d0e11f49ad | ||
|
|
ec8ed2ae2a | ||
| 4340da9b6e | |||
|
|
d8083e8de8 | ||
|
|
a1b33fc176 | ||
|
|
bb39399f97 | ||
|
|
91cfdb963d | ||
|
|
42316bc04f | ||
|
|
9aeb80dbf3 | ||
|
|
a9cae85052 | ||
|
|
dfc26d1182 | ||
|
|
7ce396931c | ||
|
|
4c08bc9c49 | ||
|
|
5227c144d7 | ||
|
|
6f1bc9896b | ||
|
|
cc6be14c1d | ||
|
|
1fad76b906 |
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# exclude vendor/
|
||||
SOURCES="./oragono.go ./irc"
|
||||
SOURCES="./ergo.go ./irc"
|
||||
|
||||
if [ "$1" = "--fix" ]; then
|
||||
exec gofmt -s -w $SOURCES
|
||||
|
||||
32
.github/workflows/build.yml
vendored
Normal file
32
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: "build"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "stable"
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "stable"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: "ubuntu-24.04"
|
||||
steps:
|
||||
- name: "checkout repository"
|
||||
uses: "actions/checkout@v3"
|
||||
- name: "setup go"
|
||||
uses: "actions/setup-go@v3"
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: "install python3-pytest"
|
||||
run: "sudo apt install -y python3-pytest"
|
||||
- name: "make install"
|
||||
run: "make install"
|
||||
- name: "make test"
|
||||
run: "make test"
|
||||
- name: "make smoke"
|
||||
run: "make smoke"
|
||||
- name: "make irctest"
|
||||
run: "make irctest"
|
||||
48
.github/workflows/docker-image.yml
vendored
Normal file
48
.github/workflows/docker-image.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: 'ghcr'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "stable"
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Authenticate to container registry
|
||||
uses: docker/login-action@v2
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Setup Docker buildx driver
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and publish image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -95,7 +95,7 @@ _testmain.go
|
||||
*.out
|
||||
|
||||
|
||||
### Oragono ###
|
||||
### custom ###
|
||||
/_site/
|
||||
/.vscode/*
|
||||
/ircd*
|
||||
@ -103,10 +103,11 @@ _testmain.go
|
||||
/web.*
|
||||
/ssl.*
|
||||
/tls.*
|
||||
/oragono
|
||||
/ergo
|
||||
/build/*
|
||||
_test
|
||||
oragono.prof
|
||||
oragono.mprof
|
||||
ergo.prof
|
||||
ergo.mprof
|
||||
/dist
|
||||
*.pem
|
||||
.dccache
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "irctest"]
|
||||
path = irctest
|
||||
url = https://github.com/oragono/irctest
|
||||
url = https://github.com/ergochat/irctest
|
||||
|
||||
@ -1,52 +1,77 @@
|
||||
# .goreleaser.yml
|
||||
# Build customization
|
||||
project_name: oragono
|
||||
version: 2
|
||||
project_name: ergo
|
||||
builds:
|
||||
- main: oragono.go
|
||||
binary: oragono
|
||||
- main: ergo.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: ergo
|
||||
goos:
|
||||
- freebsd
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
- freebsd
|
||||
- openbsd
|
||||
- plan9
|
||||
goarch:
|
||||
- "386"
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
- goos: freebsd
|
||||
goarch: riscv64
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: riscv64
|
||||
- goos: plan9
|
||||
goarch: arm
|
||||
- goos: plan9
|
||||
goarch: arm64
|
||||
- goos: plan9
|
||||
goarch: riscv64
|
||||
flags:
|
||||
- -trimpath
|
||||
|
||||
archives:
|
||||
-
|
||||
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
name_template: >-
|
||||
{{ .ProjectName }}-{{ .Version }}-
|
||||
{{- if eq .Os "darwin" }}macos{{- else }}{{ .Os }}{{ end -}}-
|
||||
{{- if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end -}}
|
||||
{{ if .Arm }}v{{ .Arm }}{{ end -}}
|
||||
format: tar.gz
|
||||
replacements:
|
||||
amd64: x86_64
|
||||
darwin: macos
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- README
|
||||
- CHANGELOG.md
|
||||
- oragono.motd
|
||||
- LICENSE
|
||||
- ergo.motd
|
||||
- default.yaml
|
||||
- traditional.yaml
|
||||
- docs/API.md
|
||||
- docs/MANUAL.md
|
||||
- docs/USERGUIDE.md
|
||||
- languages/*.yaml
|
||||
|
||||
21
.travis.yml
21
.travis.yml
@ -1,21 +0,0 @@
|
||||
language: go
|
||||
|
||||
dist: focal
|
||||
|
||||
go:
|
||||
- "1.15.x"
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- stable
|
||||
|
||||
before_install:
|
||||
# https://github.com/travis-ci/travis-ci/issues/8361
|
||||
- sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'
|
||||
|
||||
script:
|
||||
- make install
|
||||
- make test
|
||||
- make smoke
|
||||
- make irctest
|
||||
576
CHANGELOG.md
576
CHANGELOG.md
@ -1,5 +1,577 @@
|
||||
# Changelog
|
||||
All notable changes to Oragono will be documented in this file.
|
||||
All notable changes to Ergo will be documented in this file.
|
||||
|
||||
## [2.17.0-rc1] - 2025-12-14
|
||||
|
||||
We're pleased to be publishing the release candidate for v2.17.0 (the official release should follow within a week or so). This release adds support for the [IRCv3 metadata specification](https://ircv3.net/specs/extensions/metadata), thanks to [@thatcher-gaming](https://github.com/thatcher-gaming), as well as bug fixes and minor updates.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
||||
|
||||
Many thanks to [@branchgrove](https://github.com/branchgrove), [@Brutus5000](https://github.com/Brutus5000), [@progval](https://github.com/progval), [@SarahRoseLives](https://github.com/SarahRoseLives), [@thatcher-gaming](https://github.com/thatcher-gaming), [@ValwareIRC](https://github.com/ValwareIRC), and Xogium for contributing patches, reporting issues, and helping test.
|
||||
|
||||
### Config changes
|
||||
* Added `accounts.metadata` block to configure the new metadata feature. If this block is absent, metadata is disabled. See `default.yaml` for an example. (#2273)
|
||||
* Added `server.idle-timeouts` for configurable idle timeouts; when unset, the previous hardcoded defaults are used (#2292, thanks [@Brutus5000](https://github.com/Brutus5000)!)
|
||||
* Added `server.oper-throttle` to configure throttling for failed `OPER` attempts; when unset, this defaults to 1 attempt every 10 seconds (#2296)
|
||||
|
||||
### Added
|
||||
* Implemented support for the [draft/metadata-2](https://ircv3.net/specs/extensions/metadata) specification, allowing clients to set and retrieve metadata on accounts and channels (#2273, #2277, #2281, #2282, #2301, thanks [@thatcher-gaming](https://github.com/thatcher-gaming)!)
|
||||
* Added `/v1/status` and `/v1/account_list` HTTP API endpoints (#2261, thanks [@SarahRoseLives](https://github.com/SarahRoseLives)!)
|
||||
* Enhanced `/v1/account_details` API response with additional fields (#2261, thanks [@SarahRoseLives](https://github.com/SarahRoseLives)!)
|
||||
|
||||
### Fixed
|
||||
* Fixed `REGISTER` command to strip guest format when applicable, matching `NS REGISTER` behavior (#2270, #2271, thanks [@ValwareIRC](https://github.com/ValwareIRC) and [@thatcher-gaming](https://github.com/thatcher-gaming)!)
|
||||
* Fixed invalid `FAIL` codes in `REGISTER` command (#2269, thanks [@ValwareIRC](https://github.com/ValwareIRC)!)
|
||||
* Fixed validation of web push URLs to reject non-HTTPS URLs (#2295)
|
||||
* Fixed inconsistent behavior when `history.enabled` is set but `history.chathistory-maxmessages` is not (#2303, #2304, thanks [@branchgrove](https://github.com/branchgrove)!)
|
||||
|
||||
### Changed
|
||||
* The `OPER` command now imposes a throttle on all attempts, never disconnects the client on failure, and logs non-sensitive information about failed attempts (#2296, #2298, thanks Xogium!)
|
||||
|
||||
### Internal
|
||||
* Official release builds use Go 1.25 (#2290)
|
||||
* Upgraded the Docker base image from Alpine 3.19 to 3.22 (#2306)
|
||||
|
||||
## [2.16.0] - 2025-05-18
|
||||
We're pleased to be publishing v2.16.0, a new stable release. This release contains bug fixes and some minor updates.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
||||
|
||||
Many thanks to [@csmith](https://github.com/csmith), [@delthas](https://github.com/delthas), donio, [@emersion](https://github.com/emersion), [@KlaasT](https://github.com/KlaasT), [@knolley](https://github.com/knolley), [@Mailaender](https://github.com/Mailaender), and [@prdes](https://github.com/prdes) for reporting issues and helping test.
|
||||
|
||||
### Config changes
|
||||
* Added `api` block for configuring the new HTTP API. If this block is absent, the API is disabled (#2231)
|
||||
* Added `server.additional-isupport` for publishing arbitrary ISUPPORT tokens (#2220, #2240)
|
||||
* Added `server.command-aliases` to configure aliases for server commands (#2229, #2236)
|
||||
* Added options to `roleplay` to customize the NUH's sent for `NPC` and `SCENE`. Roleplay remains deprecated and disabled by default. (#2237)
|
||||
|
||||
### Security
|
||||
* Mitigated HTTP DoS attacks by rejecting IRC sessions that begin with an HTTP verb, such as `POST`. If you were relying on this to create IRC sessions via an HTTP client, please open an issue. (#2239)
|
||||
|
||||
### Added
|
||||
* Added an HTTP API, providing programmatic access to Ergo functionality (#2231, thanks [@KlaasT](https://github.com/KlaasT)!)
|
||||
* Added SAFERATE to 005 ISUPPORT tokens (#2223, thanks [@delthas](https://github.com/delthas)!)
|
||||
* Added support for ed25519-sha256 for DKIM. However, enabling this algorithm is not recommended since mainstream email providers still do not support it. (#1041, #2242)
|
||||
|
||||
### Fixed
|
||||
* Fixed `CHATHISTORY TARGETS` from MySQL backend reporting incorrect timestamps when the server timezone is not UTC (#2224)
|
||||
* Fixed batch name parameter in `draft/isupport` responses (#2253)
|
||||
* Fixed `NS UNREGISTER` not deleting the stored push subscriptions (#2254)
|
||||
* Fixed cases where `NS SAREGISTER` could create clients without applying the default user modes (#2252, #2254, thanks donio!)
|
||||
* Improved validation of `CHATHISTORY` parameters (#2248, #2249, thanks [@prdes](https://github.com/prdes)!)
|
||||
* Added validation to ensure the MOTD is UTF-8 when `enforce-utf8` is enabled (the recommended default) (#2228, #2233, thanks [@KlaasT](https://github.com/KlaasT)!)
|
||||
* The client's own `QUIT` line now respects the `server-time` capability (#2218, #2219)
|
||||
* Fixed sending unnecessary replies to certain invalid `MODE` changes (#2213)
|
||||
* Improved safety of ISUPPORT length limits (#2241)
|
||||
|
||||
### Changed
|
||||
* The `draft/message-redaction` capability is no longer advertised when `allow-individual-delete` is disabled (#2215, #2216, thanks [@delthas](https://github.com/delthas)!)
|
||||
* Receiving the UTF-8 BOM (byte-order mark) at the start of an IRC connection now produces an explicit error (#2244, #2247, thanks [@csmith](https://github.com/csmith), [@Mailaender](https://github.com/Mailaender)!)
|
||||
|
||||
### Internal
|
||||
* Release builds use Go 1.24.3 (#2217)
|
||||
|
||||
## [2.15.0] - 2025-01-26
|
||||
|
||||
We're pleased to be publishing v2.15.0, a new stable release. This release adds support for mobile push notifications, via the [draft/webpush](https://github.com/ircv3/ircv3-specifications/pull/471) specification. More information on this is available in the [manual](https://github.com/ergochat/ergo/blob/ab2d842b270d9df217c779df9c7a5c594d85fdd5/docs/MANUAL.md#push-notifications) and [user guide](https://github.com/ergochat/ergo/blob/ab2d842b270d9df217c779df9c7a5c594d85fdd5/docs/USERGUIDE.md#push-notifications). This feature is still considered to be in an experimental state; `default.yaml` ships with it disabled, and its configuration may have backwards-incompatible changes in the future.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading.
|
||||
|
||||
This release includes a database change. If you have `datastore.autoupgrade` set to `true` in your configuration, it will be automatically applied when you restart Ergo. Otherwise, you can update the database manually by running `ergo upgradedb` (see the manual for complete instructions).
|
||||
|
||||
Many thanks to [@delthas](https://github.com/delthas), [@donatj](https://github.com/donatj), donio, [@emersion](https://github.com/emersion), and [@eskimo](https://github.com/eskimo) for contributing patches and helping test.
|
||||
|
||||
### Config changes
|
||||
* Added `webpush` block to the config file to configure push notifications. See `default.yaml` for an example. Note that at this time, `default.yaml` ships with support for push notifications disabled; operators can enable them by setting `webpush.enabled: true`. In the absence of such a block, push notifications are disabled.
|
||||
* We recommend the addition of `"WEBPUSH": 1` to `fakelag.command-budgets`, to speed up mobile reattach when web push is enabled. See `default.yaml` for an example.
|
||||
|
||||
### Added
|
||||
* Added support for the [draft/webpush](https://github.com/ircv3/ircv3-specifications/pull/471) specification (#2205, thanks [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo)!)
|
||||
* Added support for the [draft/extended-isupport](https://github.com/ircv3/ircv3-specifications/pull/543) specification (#2184, thanks [@emersion](https://github.com/emersion)!)
|
||||
* `UBAN ADD` now accepts `REQUIRE-SASL` with NUH masks, i.e. k-lines (#2198, #2199)
|
||||
* Ergo now publishes the `SAFELIST` ISUPPORT parameter (#2196, thanks [@delthas](https://github.com/delthas)!)
|
||||
|
||||
### Fixed
|
||||
* Fixed incorrect parameters when pushing `005` (ISUPPORT) updates to clients on rehash (#2177, #2184)
|
||||
|
||||
### Internal
|
||||
* Official release builds use Go 1.23.5
|
||||
* Added a unique identifier to identify connections in debug logs. This has no privacy implications in a standard, non-debug configuration of Ergo. (#2206, thanks donio!)
|
||||
* Added support for Solaris on amd64 CPUs (#2183)
|
||||
|
||||
## [2.14.0] - 2024-06-30
|
||||
|
||||
We're pleased to be publishing v2.14.0, a new stable release. This release contains primarily bug fixes, with the addition of some new authentication mechanisms for integrating with web clients.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
||||
|
||||
Many thanks to [@al3xandros](https://github.com/al3xandros), donio, [@eeeeeta](https://github.com/eeeeeta), [@emersion](https://github.com/emersion), [@Eriner](https://github.com/Eriner), [@eskimo](https://github.com/eskimo), [@Herringway](https://github.com/Herringway), [@jwheare](https://github.com/jwheare), [@knolley](https://github.com/knolley), [@mengzhuo](https://github.com/mengzhuo), pathof, [@poVoq](https://github.com/poVoq), [@progval](https://github.com/progval), [@RNDpacman](https://github.com/RNDpacman), and [@xnaas](https://github.com/xnaas) for contributing patches, reporting issues, and helping test.
|
||||
|
||||
### Config changes
|
||||
* Added `accounts.oauth2` and `accounts.jwt-auth` blocks for configuring OAuth2 and JWT authentication (#2004)
|
||||
* Added `protocol` and `local-address` options to `accounts.registration.email-verification`, to force emails to be sent over IPv4 (or IPv6) or to force the use of a particular source address (#2142)
|
||||
* Added `limits.realnamelen`, a configurable limit on the length of realnames. If unset, no limit is enforced beyond the IRC protocol line length limits (the previous behavior). (#2123, thanks [@eskimo](https://github.com/eskimo)!)
|
||||
* Added the `accept-hostname` option to the webirc config block, allowing Ergo to accept hostnames passed from reverse proxies on the `WEBIRC` line. Note that this will have no effect under the default/recommended configuration, in which cloaks are used instead (#1686, #2146, thanks [@RNDpacman](https://github.com/RNDpacman)!)
|
||||
* The default/recommended value of `limits.chan-list-modes` (the size limit for ban/except/invite lists) was raised to 100 (#2081, #2165, #2167)
|
||||
|
||||
### Added
|
||||
* Added support for the `OAUTHBEARER` SASL mechanism, allowing Ergo to interoperate with Gamja and an OAuth2 provider (#2004, #2122, thanks [@emersion](https://github.com/emersion)!)
|
||||
* Added support for the [`IRCV3BEARER` SASL mechanism](https://github.com/ircv3/ircv3-specifications/pull/545), allowing Ergo to accept OAuth2 or JWT bearer tokens (#2158)
|
||||
* Added support for the legacy `rfc1459` and `rfc1459-strict` casemappings (#2099, #2159, thanks [@xnaas](https://github.com/xnaas)!)
|
||||
* The new `ergo defaultconfig` subcommand prints a copy of the default config file to standard output (#2157, #2160, thanks [@al3xandros](https://github.com/al3xandros)!)
|
||||
|
||||
### Fixed
|
||||
* Even with `allow-truncation: false` (the recommended default), some oversized messages were being accepted and relayed with truncation. These messages will now be rejected with `417 ERR_INPUTTOOLONG` as expected (#2170)
|
||||
* NICK and QUIT from invisible members of auditorium channels are no longer recorded in history (#2133, #2137, thanks [@knolley](https://github.com/knolley) and [@poVoq](https://github.com/poVoq)!)
|
||||
* If channel registration was disabled, registered channels could become inaccessible after rehash; this has been fixed (#2130, thanks [@eeeeeta](https://github.com/eeeeeta)!)
|
||||
* Attempts to use unrecognized SASL mechanisms no longer count against the login throttle, improving compatibility with Pidgin (#2156, thanks donio and pathof!)
|
||||
* Fixed database autoupgrade on Windows, which was previously broken due to the use of a colon in the backup filename (#2139, #2140, thanks [@Herringway](https://github.com/Herringway)!)
|
||||
* Fixed handling of `NS CERT ADD <user> <fp>` when an unprivileged user invokes it on themself (#2128, #2098, thanks [@Eriner](https://github.com/Eriner)!)
|
||||
* Fixed missing human-readable trailing parameters for two multiline `FAIL` messages (#2043, #2162, thanks [@jwheare](https://github.com/jwheare) and [@progval](https://github.com/progval)!)
|
||||
* Fixed symbol sent by `353 RPL_NAMREPLY` for secret channels (#2144, #2145, thanks savoyard!)
|
||||
|
||||
### Changed
|
||||
* Trying to claim a registered nickname that is also actually in use by another client now produces `433 ERR_NICKNAMEINUSE` as expected (#2135, #2136, thanks savoyard!)
|
||||
* `SAMODE` now overrides the enforcement of `limits.chan-list-modes` (the size limit for ban/except/invite lists) (#2081, #2165)
|
||||
* Certain unsuccessful `MODE` changes no longer send `324 RPL_CHANNELMODEIS` and `329 RPL_CREATIONTIME` (#2163)
|
||||
* Debug logging for environment variable configuration overrides no longer prints the value, only the key (#2129, #2132, thanks [@eeeeeta](https://github.com/eeeeeta)!)
|
||||
|
||||
### Internal
|
||||
|
||||
* Official release builds use Go 1.22.4
|
||||
* Added a linux/riscv64 release (#2172, #2173, thanks [@mengzhuo](https://github.com/mengzhuo)!)
|
||||
|
||||
## [2.13.1] - 2024-05-06
|
||||
|
||||
Ergo 2.13.1 is a bugfix release, fixing an exploitable deadlock that could lead to a denial of service. We regret the oversight.
|
||||
|
||||
This release includes no changes to the config file format or database format.
|
||||
|
||||
### Security
|
||||
|
||||
* Fixed an exploitable deadlock that could lead to a denial of service (#2149)
|
||||
|
||||
### Internal
|
||||
|
||||
* Official release builds use Go 1.22.2
|
||||
|
||||
|
||||
## [2.13.0] - 2024-01-14
|
||||
|
||||
We're pleased to be publishing v2.13.0, a new stable release. This is a bugfix release that fixes some issues, including a crash.
|
||||
|
||||
This release includes no changes to the config file format or database format.
|
||||
|
||||
Many thanks to [@dallemon](https://github.com/dallemon), [@jwheare](https://github.com/jwheare), [@Mikaela](https://github.com/Mikaela), [@nealey](https://github.com/nealey), and [@Sheikah45](https://github.com/Sheikah45) for contributing patches, reporting issues, and helping test.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a (hopefully rare) crash when persisting always-on client statuses (#2113, #2117, thanks [@Sheikah45](https://github.com/Sheikah45)!)
|
||||
* Fixed not being able to message channels with `/` (or the configured `RELAYMSG` separator) in their names (#2114, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Verification emails now always include a `Message-ID` header, improving compatibility with Gmail (#2108, #2110)
|
||||
* Improved human-readable description of `REDACT_FORBIDDEN` (#2101, thanks [@jwheare](https://github.com/jwheare)!)
|
||||
|
||||
### Removed
|
||||
|
||||
* Removed numerics associated with the retired ACC spec (#2109, #2111, thanks [@jwheare](https://github.com/jwheare)!)
|
||||
|
||||
### Internal
|
||||
|
||||
* Upgraded the Docker base image from Alpine 3.13 to 3.19. The resulting images are incompatible with Docker 19.x and lower (all currently non-EOL Docker versions should be supported). (#2103)
|
||||
* Official release builds use Go 1.21.6
|
||||
|
||||
|
||||
## [2.12.0] - 2023-10-10
|
||||
|
||||
We're pleased to be publishing v2.12.0, a new stable release. This is another bugfix release aimed at improving client compatibility and keeping up with the IRCv3 specification process.
|
||||
|
||||
This release includes changes to the config file format, one of which is a compatibility break: if you were using `accounts.email-verification.blacklist-regexes`, you can restore the previous functionality by renaming `blacklist-regexes` to `address-blacklist` and setting the additional key `address-blacklist-syntax: regex`. See [default.yaml](https://github.com/ergochat/ergo/blob/e7597876d987a6fc061b768fcf878d0035d1c85a/default.yaml#L422-L424) for an example; for more details, see the "Changed" section below.
|
||||
|
||||
This release includes a database change. If you have `datastore.autoupgrade` set to `true` in your configuration, it will be automatically applied when you restart Ergo. Otherwise, you can update the database manually by running `ergo upgradedb` (see the manual for complete instructions).
|
||||
|
||||
Many thanks to [@adsr](https://github.com/adsr), [@avollmerhaus](https://github.com/avollmerhaus), [@csmith](https://github.com/csmith), [@EchedeyLR](https://github.com/EchedeyLR), [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@julio-b](https://github.com/julio-b), knolle, [@KoxSosen](https://github.com/KoxSosen), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), and [@progval](https://github.com/progval) for contributing patches, reporting issues, and helping test.
|
||||
|
||||
### Config changes
|
||||
* Removed `accounts.email-verification.blacklist-regexes` in favor of `address-blacklist`, `address-blacklist-syntax`, and `address-blacklist-file`. See the "Changed" section below for the semantics of these new keys. (#1997, #2088)
|
||||
* Added `implicit-tls` (TLS from the first byte) support for MTAs (#2048, #2049, thanks [@EchedeyLR](https://github.com/EchedeyLR)!)
|
||||
|
||||
### Fixed
|
||||
* Fixed an edge case under `allow-truncation: true` (the recommended default is `false`) where Ergo could truncate a message in the middle of a UTF-8 codepoint (#2074)
|
||||
* Fixed `CHATHISTORY TARGETS` being sent in a batch even without negotiation of the `batch` capability (#2066, thanks [@julio-b](https://github.com/julio-b)!)
|
||||
* Errors from `/REHASH` are now properly sanitized before being sent to the user, fixing an edge case where they would be dropped (#2031, thanks [@eskimo](https://github.com/eskimo)!
|
||||
* Fixed some edge cases in auto-away aggregation (#2044)
|
||||
* Fixed a FAIL code sent by draft/account-registration (#2092, thanks [@progval](https://github.com/progval)!)
|
||||
* Fixed a socket leak in the ident client (default/recommended configurations of Ergo disable ident and are not affected by this issue) (#2089)
|
||||
|
||||
### Changed
|
||||
* Bouncer reattach from an "insecure" session is no longer disallowed. We continue to recommend that operators preemptively disable all insecure transports, such as plaintext listeners (#2013)
|
||||
* Email addresses are now converted to lowercase before checking them against the blacklist (#1997, #2088)
|
||||
* The default syntax for the email address blacklist is now "glob" (expressions with `*` and `?` as wildcard characters), as opposed to the full [Go regular expression syntax](https://github.com/google/re2/wiki/Syntax). To enable full regular expression syntax, set `address-blacklist-syntax: regex`.
|
||||
* Due to line length limitations, some capabilities are now hidden from clients that only support version 301 CAP negotiation. To the best of our knowledge, all clients that support these capabilities also support version 302 CAP negotiation, rendering this moot (#2068)
|
||||
* The default/recommended configuration now advertises the SCRAM-SHA-256 SASL method. We still do not recommend using this method in production. (#2032)
|
||||
* Improved KILL messages (#2053, #2041, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
|
||||
### Added
|
||||
* Added support for automatically joining new clients to a channel or channels (#2077, #2079, thanks [@adsr](https://github.com/adsr)!)
|
||||
* Added implicit TLS (TLS from the first byte) support for MTAs (#2048, #2049, thanks [@EchedeyLR](https://github.com/EchedeyLR)!)
|
||||
* Added support for [draft/message-redaction](https://github.com/ircv3/ircv3-specifications/pull/524) (#2065, thanks [@progval](https://github.com/progval)!)
|
||||
* Added support for [draft/pre-away](https://github.com/ircv3/ircv3-specifications/pull/514) (#2044)
|
||||
* Added support for [draft/no-implicit-names](https://github.com/ircv3/ircv3-specifications/pull/527) (#2083)
|
||||
* Added support for the [MSGREFTYPES](https://ircv3.net/specs/extensions/chathistory#isupport-tokens) 005 token (#2042)
|
||||
* Ergo now advertises the [standard-replies](https://ircv3.net/specs/extensions/standard-replies) capability. Requesting this capability does not change Ergo's behavior.
|
||||
|
||||
### Internal
|
||||
* Release builds are now statically linked by default. This should not affect normal chat operations, but may disrupt attempts to connect to external services (e.g. MTAs) that are configured using a hostname that relies on libc's name resolution behavior. To restore the old behavior, build from source with `CGO_ENABLED=1`. (#2023)
|
||||
* Upgraded to Go 1.21 (#2045, #2084); official release builds use Go 1.21.3, which includes a fix for CVE-2023-44487
|
||||
* The default `make` target is now `build` (which builds an `ergo` binary in the working directory) instead of `install` (which builds and installs an `ergo` binary to `${GOPATH}/bin/ergo`). Take note if building from source, or testing Ergo in development! (#2047)
|
||||
* `make irctest` now depends on `make install`, in an attempt to ensure that irctest runs against the intended development version of Ergo (#2047)
|
||||
|
||||
## [2.11.1] - 2022-01-22
|
||||
|
||||
Ergo 2.11.1 is a bugfix release, fixing a denial-of-service issue in our websocket implementation. We regret the oversight.
|
||||
|
||||
This release includes no changes to the config file format or database file format.
|
||||
|
||||
### Security
|
||||
* Fixed a denial-of-service issue affecting websocket clients (#2039)
|
||||
|
||||
## [2.11.0] - 2022-12-25
|
||||
|
||||
We're pleased to be publishing v2.11.0, a new stable release. This is another bugfix release aimed at improving client compatibility and keeping up with the IRCv3 specification process.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
||||
|
||||
Many thanks to dedekro, [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@FiskFan1999](https://github.com/FiskFan1999), hauser, [@jwheare](https://github.com/jwheare), [@kingter-sutjiadi](https://github.com/kingter-sutjiadi), knolle, [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@PeGaSuS-Coder](https://github.com/PeGaSuS-Coder), and [@progval](https://github.com/progval) for contributing patches, reporting issues, and helping test.
|
||||
|
||||
### Config changes
|
||||
|
||||
* Added `fakelag.command-budgets`, which allows each client session a limited number of specific commands that are exempt from fakelag. This improves compatibility with Goguma in particular. For the current recommended default, see `default.yaml` (#1978, thanks [@emersion](https://github.com/emersion)!)
|
||||
* The recommended value of `server.casemapping` is now `ascii` instead of `precis`. PRECIS remains fully supported; if you are already running an Ergo instance, we do not recommend changing the value unless you are confident that your existing users are not relying on non-ASCII nicknames and channel names. (#1718)
|
||||
|
||||
### Changed
|
||||
|
||||
* Network services like `NickServ` now appear in `WHO` responses where applicable (#1850, thanks [@emersion](https://github.com/emersion)!)
|
||||
* The `extended-monitor` capability now appears under its ratified name (#2006, thanks [@progval](https://github.com/progval)!)
|
||||
* `TAGMSG` no longer receives automatic `RPL_AWAY` responses (#1983, thanks [@eskimo](https://github.com/eskimo)!)
|
||||
* `UBAN` now states explicitly that bans without a time limit have "indefinite" duration (#1988, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
|
||||
### Fixed
|
||||
|
||||
* `WHO` with a bare nickname as an argument now shows invisible users, comparable to `WHOIS` (#1991, thanks [@emersion](https://github.com/emersion)!)
|
||||
* MySQL did not work on 32-bit architectures; this has been fixed (#1969, thanks hauser!)
|
||||
* Fixed the name of the `CHATHISTORY` 005 token (#2008, #2009, thanks [@emersion](https://github.com/emersion)!)
|
||||
* Fixed handling of the address `::1` in WHOX output (#1980, thanks knolle!)
|
||||
* Fixed handling of `AWAY` with an empty parameter (the de facto standard is to treat as a synonym for no parameter, which means "back") (#1996, thanks [@emersion](https://github.com/emersion), [@jwheare](https://github.com/jwheare)!)
|
||||
* Fixed incorrect handling of some invalid modes in `CS AMODE` (#2002, thanks [@eskimo](https://github.com/eskimo)!)
|
||||
* Fixed incorrect help text for `NS SAVERIFY` (#2021, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
|
||||
|
||||
### Added
|
||||
|
||||
* Added the `draft/persistence` capability and associated `PERSISTENCE` command. This is a first attempt to standardize Ergo's "always-on" functionality so that clients can interact with it programmatically. (#1982)
|
||||
* Sending `SIGUSR1` to the Ergo process now prints a full goroutine stack dump to stderr, allowing debugging even when the HTTP pprof listener is disabled (#1975)
|
||||
|
||||
### Internal
|
||||
|
||||
* Upgraded to Go 1.19; this makes further architecture-specific bugs like #1969 much less likely (#1987, #1989)
|
||||
* The test suite is now parallelized (#1976, thanks [@progval](https://github.com/progval)!)
|
||||
|
||||
|
||||
## [2.10.0] - 2022-05-29
|
||||
|
||||
We're pleased to be publishing v2.10.0, a new stable release.
|
||||
|
||||
This release contains no changes to the config file format or database file format.
|
||||
|
||||
Many thanks to [@csmith](https://github.com/csmith), [@FiskFan1999](https://github.com/FiskFan1999), [@Mikaela](https://github.com/Mikaela), [@progval](https://github.com/progval), and [@thesamesam](https://github.com/thesamesam) for contributing patches, and to [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@FiskFan1999](https://github.com/FiskFan1999), [@jigsy1](https://github.com/jigsy1), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@progval](https://github.com/progval), and [@xnaas](https://github.com/xnaas) for reporting issues and helping test.
|
||||
|
||||
### Config changes
|
||||
|
||||
* For better interoperability with [Goguma](https://sr.ht/~emersion/goguma/), the recommended value of `history.chathistory-maxmessages` has been increased to `1000` (previously `100`) (#1919)
|
||||
|
||||
### Changed
|
||||
* Persistent voice (`AMODE +v`) in a channel is now treated as a permanent invite (i.e. overriding `+i` on the channel) (#1901, thanks [@eskimo](https://github.com/eskimo)!)
|
||||
* If you are `+R`, sending a direct message to an anonymous user allows them to send you replies (#1687, #1688, thanks [@Mikaela](https://github.com/Mikaela) and [@progval](https://github.com/progval)!)
|
||||
* `0` is no longer valid as a nickname or account name, with a grandfather exception if it was registered on a previous version of Ergo (#1896)
|
||||
* Implemented the [ratified version of the bot mode spec](https://ircv3.net/specs/extensions/bot-mode); the tag name is now `bot` instead of `draft/bot` (#1938)
|
||||
* Privileged WHOX on a user with multiclient shows an arbitrarily chosen client IP address, comparable to WHO (#1897)
|
||||
* `SAREGISTER` is allowed even under `DEFCON` levels 4 and lower (#1922)
|
||||
* Operators with the `history` capability are now exempted from time cutoff restrictions on history retrieval (#1593, #1955)
|
||||
|
||||
### Added
|
||||
* Added `draft/read-marker` capability, allowing server-side tracking of read messages for synchronization across multiple clients. (#1926, thanks [@emersion](https://github.com/emersion)!)
|
||||
* `INFO` now includes the server start time (#1895, thanks [@xnaas](https://github.com/xnaas)!)
|
||||
* Added `ACCEPT` command modeled on Charybdis/Solanum, allowing `+R` users to whitelist users who can DM them (#1688, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Added `NS SAVERIFY` for operators to manually complete an account verification (#1924, #1952, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
|
||||
### Fixed
|
||||
* Having the `samode` operator capability made all uses of the `KICK` command privileged (i.e. overriding normal channel privilege checks); this has been fixed (#1906, thanks [@pcho](https://github.com/pcho)!)
|
||||
* Fixed `LIST <n` always returning no results (#1934, thanks [@progval](https://github.com/progval) and [@mitchr](https://github.com/mitchr)!)
|
||||
* NickServ commands are now more clear about when a nickname is unavailable because it was previously registered and unregistered (#1886, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed KLINE'd clients producing a `QUIT` snotice without a corresponding `CONNECT` snotice (#1941, thanks [@tacerus](https://github.com/tacerus), [@xnaas](https://github.com/xnaas)!)
|
||||
* Fixed incorrect handling of long/multiline `319 RPL_WHOISCHANNELS` responses (#1935, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed `LIST` returning `403 ERR_NOSUCHCHANNEL` for a nonexistent channel; the correct response is an empty list (#1928, thanks [@emersion](https://github.com/emersion)!)
|
||||
* Fixed `+s` ("secret") channels not appearing in `LIST` even when the client is already a member (#1911, #1923, thanks [@jigsy1](https://github.com/jigsy1) and [@FiskFan1999](https://github.com/FiskFan1999)!)
|
||||
* Fixed a spurious success message in `HISTSERV DELETE` by always requiring a consistent number of parameters (#1881, #1927, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
|
||||
* Sending the empty string as a nickname would not always produce the expected error numeric `431 ERR_NONICKNAMEGIVEN`; this has been fixed (#1933, #1936, thanks [@kylef](https://github.com/kylef)!)
|
||||
* `znc.in/playback` timestamps are now parsed as pairs of exact integers, not as floats (#1918)
|
||||
|
||||
### Internal
|
||||
* Upgraded to Go 1.18 (#1925)
|
||||
* Upgraded Alpine version in official Docker image
|
||||
* Fixed some issues in the example OpenRC init scripts (#1914, #1920, thanks [@thesamesam](https://github.com/thesamesam)!)
|
||||
|
||||
|
||||
## [2.9.1] - 2022-01-10
|
||||
|
||||
Ergo 2.9.1 is a bugfix release, fixing a regression introduced in 2.9.0. We regret the oversight.
|
||||
|
||||
This release includes no changes to the config file format or database format relative to 2.9.0.
|
||||
|
||||
Many thanks to [@FiskFan1999](https://github.com/FiskFan1999) for reporting the issue.
|
||||
|
||||
### Fixed
|
||||
* Every use of NS SAREGISTER would fail; this has been fixed (#1898, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
|
||||
|
||||
|
||||
## [2.9.0] - 2022-01-09
|
||||
|
||||
We're pleased to be publishing 2.9.0, a new stable release. This release contains mostly bug fixes, with some enhancements to moderation tools.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
||||
|
||||
Many thanks to [@erincerys](https://github.com/erincerys), [@FiskFan1999](https://github.com/FiskFan1999), [@mogad0n](https://github.com/mogad0n), and [@tacerus](https://github.com/tacerus) for contributing patches, and to [@ajaspers](https://github.com/ajaspers), [@emersion](https://github.com/emersion), [@FiskFan1999](https://github.com/FiskFan1999), [@Jobe1986](https://github.com/Jobe1986), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@pcho](https://github.com/pcho), and [@progval](https://github.com/progval) for reporting issues and helping test.
|
||||
|
||||
|
||||
### Config changes
|
||||
* Added `lock-file`, which helps protect against accidentally starting multiple instances of Ergo. This is a no-op if unset. The recommended default value is `ircd.lock`, which (like the default datastore path `ircd.db`) is relative to the working directory of the Ergo process. If your `datastore.path` is absolute, this path (if set) should be absolute as well. (#1823)
|
||||
* `+C` (no channel-wide CTCP messages other than ACTION) is now a recommended default channel mode (#1851)
|
||||
* Added `exempt-sasl` boolean to `server.ip-check-script`; if enabled, IP check scripts are run only for connections without SASL, improving performance for registered users (#1888)
|
||||
* `hidden: true` is now the recommended default for operator definitions (#1730)
|
||||
|
||||
### Changed
|
||||
* The semantics of `+R` have been changed. `+R` now only prevents unauthenticated users from joining, so unregistered users who have already joined can still speak. The old semantics are still available via `+RM` (i.e. `+R` together with the `+M` "moderated-registered" mode). (#1858, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Unauthenticated users matching a `+I` invite exception mask can now join `+R` channels (#1871)
|
||||
* INVITE now exempts the user from `+b` bans (#1876, thanks [@progval](https://github.com/progval)!)
|
||||
* NS SUSPEND now only requires only the `ban` operator capability, as opposed to `accreg` (#1828, #1839, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
|
||||
### Added
|
||||
* SHA-256 certificate fingerprints can now be imported from Anope and Atheme (#1864, #1869, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
* IP check scripts can now be run only for users that have not authenticated with SASL by the end of the handshake, improving performance for registered users (#1888)
|
||||
* Logging into an unverified account with SASL sends the new `NOTE AUTHENTICATE VERIFICATION_REQUIRED` [standard reply code](https://ircv3.net/specs/extensions/standard-replies) (#1852, #1853, thanks [@emersion](https://github.com/emersion)!)
|
||||
* CS PURGE now sends a snotice (#1826, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
* The `v` snomask is now used to send notifications about vhost changes initiated by operators (#1844, thanks [@pcho](https://github.com/pcho)!)
|
||||
|
||||
### Fixed
|
||||
* CAP LS and LIST responses after connection registration could be truncated in some cases; this has been fixed (#1872)
|
||||
* Unprivileged users with both a password and a certfp could not remove their password with `NS PASSWD <password> * *` as expected; this has been fixed (#1883, #1884, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
|
||||
* RELAYMSG identifiers that were not already in their case-normalized form could not be muted with `+b m:`; this has been fixed (#1838, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
* CS AMODE changes did not take immediate effect if `force-nick-equals-account` was disabled and the nick did not coincide with the account; this has been fixed (#1860, thanks [@eskimo](https://github.com/eskimo)!)
|
||||
* `315 RPL_ENDOFWHO` now sends the exact, un-normalized mask argument provided by the client (#1831, thanks [@progval](https://github.com/progval)!)
|
||||
* A leading `$` character is now disallowed in new nicknames and account names, to avoid collision with the massmessage syntax (#1857, thanks [@emersion](https://github.com/emersion)!)
|
||||
* The [deprecated](https://github.com/ircdocs/modern-irc/pull/138) `o` parameter of `WHO` now returns an empty list of results, instead of being ignored (#1730, thanks [@kylef](https://github.com/kylef), [@emersion](https://github.com/emersion), [@progval](https://github.com/progval)!)
|
||||
* WHOX queries for channel oplevel now receive `*` instead of `0` (#1866, thanks [@Jobe1986](https://github.com/Jobe1986)!)
|
||||
|
||||
### Internal
|
||||
* Updated list of official release binaries: added Apple M1, OpenBSD x86-64, and Plan 9 x86-64, removed Linux armv7, FreeBSD x86-32, and Windows x86-32. (The removed platforms are still fully supported by Ergo; you can build them from source or ask us for help.) (#1833)
|
||||
* Added an official Linux arm64 Docker image (#1855, thanks [@erincerys](https://github.com/erincerys)!)
|
||||
* Added service management files for OpenSolaris/Illumos (#1846, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
|
||||
|
||||
## [2.8.0] - 2021-11-14
|
||||
|
||||
We're pleased to be publishing Ergo 2.8.0. This release contains many fixes and enhancements, plus one major user-facing feature: user-initiated password resets via e-mail (#734).
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading.
|
||||
|
||||
This release includes a database change. If you have `datastore.autoupgrade` set to `true` in your configuration, it will be automatically applied when you restart Ergo. Otherwise, you can update the database manually by running `ergo upgradedb` (see the manual for complete instructions).
|
||||
|
||||
As part of this release, our official Docker images have moved from Docker Hub to the GitHub Container Registry, at `ghcr.io/ergochat/ergo`. The `stable` and `master` tags correspond to the respective branches. Tagged releases (e.g. `v2.8.0`) are available under the corresponding named tags.
|
||||
|
||||
Many thanks to [@ajaspers](https://github.com/ajaspers), [@delthas](https://github.com/delthas), [@mogad0n](https://github.com/mogad0n), [@majiru](https://github.com/majiru), [@ProgVal](https://github.com/ProgVal), and [@tacerus](https://github.com/tacerus) for contributing patches, to [@ajaspers](https://github.com/ajaspers) for contributing code review, to [@ajaspers](https://github.com/ajaspers), [@cxxboy](https://github.com/cxxboy), [@dallemon](https://github.com/dallemon), [@emersion](https://github.com/emersion), [@erikh](https://github.com/erikh), [@eskimo](https://github.com/eskimo), [@jwheare](https://github.com/jwheare), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@MystaraTheGreat](https://github.com/MystaraTheGreat), [@ProgVal](https://github.com/ProgVal), [@tacerus](https://github.com/tacerus), [@tamiko](https://github.com/tamiko), and [@xnaas](https://github.com/xnaas) for reporting issues and helping test, and to our translators for contributing translations.
|
||||
|
||||
### Config changes
|
||||
* Added `accounts.registration.email-verification.password-reset` block to configure e-mail-based password reset (#734, #1779)
|
||||
* Added `accounts.registration.email-verification.timeout` to impose a timeout on e-mail sending; the recommended default value is `60s` (60 seconds) (#1741)
|
||||
* Added `server.suppress-lusers` to allow hiding the LUSERS counts (#1802, thanks [@eskimo](https://github.com/eskimo)!)
|
||||
|
||||
### Security
|
||||
* Added `accounts.registration.email-verification.timeout` to impose a timeout on e-mail sending; the recommended default value is `60s` (60 seconds) (#1741)
|
||||
|
||||
### Added
|
||||
* Added user-initiated password resets via email (#734). This requires e-mail verification of accounts, and must additionally be enabled explicitly: see the `email-verification` block in `default.yaml` for more information.
|
||||
* Added the `draft/extended-monitor` capability (#1761, thanks [@delthas](https://github.com/delthas)!)
|
||||
* When doing direct sending of verification emails, make email delivery failures directly visible to the end user (#1659, #1741, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
* For operators, `NS INFO` now shows the user's email address (you can also view your own address) (#1677, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Operators with the appropriate permissions will now see IPs in `/WHOWAS` output (#1702, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Added the `+s d` snomask, for operators to receive information about session disconnections that do not result in a full QUIT (#1709, #1728, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
* Added support for the `SCRAM-SHA-256` SASL authentication mechanism (#175). This mechanism is not currently advertised in `CAP LS` output because IRCCloud handles it incorrectly. We also [recommend against using SCRAM because of its lack of genuine security benefits](https://gist.github.com/slingamn/3f2fed196df5ef14d1316a1ffa9d59f8).
|
||||
* `/UBAN LIST` output now includes the time the ban was created (#1725, #1755, thanks [@Mikaela](https://github.com/Mikaela) and [@mogad0n](https://github.com/mogad0n)!)
|
||||
* Added support for running as a `Type=notify` systemd service (#1733)
|
||||
* Added a warning to help users detect incorrect uses of `/QUOTE` (#1530)
|
||||
|
||||
### Fixed
|
||||
* The `+M` (only registered users can speak) channel mode did not work; this has been fixed (#1696, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* A channel `/RENAME` that only changed the case of the channel would delete the channel registration; this has been fixed (#1751, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed `allow-truncation: true` not actually allowing truncation of overlong lines (#1766, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
* Fixed several pagination bugs in `CHATHISTORY` (#1676, thanks [@emersion](https://github.com/emersion)!)
|
||||
* Fixed support for kicking multiple users from a channel on the same line, the `TARGMAX` 005 parameter that advertises this, and the default kick message (#1748, #1777, #1776), thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* Fixed `/SAMODE` on a channel not producing a snomask (#1787, thanks [@mogad0n](https://github.com/mogad0n), [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Adding `+f` to a channel with `SAMODE` used to require channel operator privileges on the receiving channel; this has been fixed (#1825, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed parameters sent with `697 ERR_LISTMODEALREADYSET` and `698 ERR_LISTMODENOTSET` (#1727, thanks [@kylef](https://github.com/kylef)!)
|
||||
* Fixed parameter sent with `696 ERR_INVALIDMODEPARAM` (#1773, thanks [@kylef](https://github.com/kylef)!)
|
||||
* Fixed handling of channel mode `+k` with an empty parameter (#1774, #1775, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* `WHOWAS` with an empty string as the parameter now produces an appropriate error response (#1703, thanks [@kylef](https://github.com/kylef)!)
|
||||
* Fixed error response to an empty realname on the `USER` line (#1778, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* Fixed `/UBAN ADD` of a NUH mask (i.e. a k-line) not killing affected clients (#1736, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
* Fixed buggy behavior when `+i` is configured as a default mode for channels (#1756, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed issues with `channels.operator-only-creation` not respecting `/SAJOIN` or always-on clients (#1757)
|
||||
* Protocol-breaking operator vhosts are now disallowed during config validation (#1722)
|
||||
* Fixed error message associated with `/NS PASSWD` on a nonexistent account (#1738, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed an incorrect `CHATHISTORY` fail message (#1731, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* Fixed a panic on an invalid configuration case (#1714, thanks [@erikh](https://github.com/erikh)!)
|
||||
|
||||
### Changed
|
||||
* Upgraded the `draft/register` capability to the latest [`draft/account-registration`](https://github.com/ircv3/ircv3-specifications/pull/435) iteration (#1740)
|
||||
* Unregistered users with `+v` or higher can now speak in `+R` (registered-only) channels (#1715, thanks [@Mikaela](https://github.com/Mikaela) and [@ajaspers](https://github.com/ajaspers)!)
|
||||
* For always-on clients with at least one active connection, `338 RPL_WHOISACTUALLY` now displays an arbitrarily chosen client IP address (#1650, thanks [@MystaraTheGreat](https://github.com/MystaraTheGreat)!)
|
||||
* `#` can no longer be used in new account names and nicknames, or as the RELAYMSG separator (#1679)
|
||||
* The `oragono.io/nope` capability was renamed to `ergo.chat/nope` (#1793)
|
||||
|
||||
### Removed
|
||||
* `never` is no longer accepted as a value of the `replay-joins` NickServ setting (`/NS SET replay-joins`); user accounts which enabled this setting have been reverted to the default value of `commands-only` (#1676)
|
||||
|
||||
### Internal
|
||||
* We have a cool new logo!
|
||||
* Official builds now use Go 1.17 (#1781)
|
||||
* Official Docker containers are now at [ghcr.io/ergochat/ergo](https://ghcr.io/ergochat/ergo) (#1808)
|
||||
* Added a traditional SysV init script (#1691, thanks [@tacerus](https://github.com/tacerus)!)
|
||||
* Added an s6 init script (#1786, thanks [@majiru](https://github.com/majiru)!)
|
||||
|
||||
## [2.7.0] - 2021-06-07
|
||||
|
||||
We're pleased to be publishing Ergo 2.7.0, our first official release under our new name of Ergo. This release contains bug fixes and minor enhancements.
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. This release includes no changes to the database format.
|
||||
|
||||
Because the name of the executable has changed from `oragono` to `ergo` (`ergo.exe` on Windows), you may need to update your system configuration (e.g., scripts or systemd unit files that reference the executable).
|
||||
|
||||
Many thanks to [@ajaspers](https://github.com/ajaspers) and [@jesopo](https://github.com/jesopo) for contributing patches, to [@ajaspers](https://github.com/ajaspers), [@ChrisTX](https://github.com/ChrisTX), [@emersion](https://github.com/emersion), [@jwheare](https://github.com/jwheare), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), and [@ProgVal](https://github.com/ProgVal) for reporting issues and helping test, and to our translators for contributing translations.
|
||||
|
||||
### Changed
|
||||
* The project was renamed from "Oragono" to "Ergo" (#897, thanks to everyone who contributed feedback or voted in the poll)
|
||||
|
||||
### Config changes
|
||||
* Entries in `server.listeners` now take a new key, `min-tls-version`, that can be used to set the minimum required TLS version; the recommended default value is `1.2` (#1611, thanks [@ChrisTX](https://github.com/ChrisTX)!)
|
||||
* Added `max-conns` (maximum connection count) and `max-conn-lifetime` (maximum lifetime of a connection before it is cycled) to `datastore.mysql` (#1622)
|
||||
* Added `massmessage` operator capability to allow sending NOTICEs to all connected users (#1153, #1629, thanks [@jesopo](https://github.com/jesopo)!)
|
||||
|
||||
### Security
|
||||
* If `require-sasl.enabled` is set to `true`, `tor-listeners.require-sasl` will be automatically set to `true` as well (#1636)
|
||||
* It is now possible to set the minimum required TLS version, using the `min-tls-version` key in listener configuration
|
||||
* Configurations that require SASL but allow user registration now produce a warning (#1637)
|
||||
|
||||
### Added:
|
||||
* Operators with the correct permissions can now send "mass messages", e.g. `/NOTICE $$*` will send a `NOTICE` to all users (#1153, #1629, thanks [@jesopo](https://github.com/jesopo)!)
|
||||
* Operators can now extend the maximum (non-tags) length of the IRC line using the `server.max-line-len` configuration key. This is not recommended for use outside of "closed-circuit" deployments where IRC operators have full control of all client software. (#1651)
|
||||
|
||||
### Fixed
|
||||
* `RELAYMSG` now sends a full NUH ("nick-user-host"), instead of only the relay nickname, as the message source (#1647, thanks [@ProgVal](https://github.com/ProgVal), [@jwheare](https://github.com/jwheare), and [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Fixed a case where channels would remain visible in `/LIST` after unregistration (#1619, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Fixed incorrect tags on `JOIN` lines in `+u` ("auditorium") channels (#1642)
|
||||
* Fixed an issue where LUSERS counts could get out of sync (#1617)
|
||||
* It was impossible to add a restricted set of snomasks to an operator's permissions; this has been fixed (#1618)
|
||||
* Fixed incorrect language in `NS INFO` responses (#1627, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Fixed a case where the `REGISTER` command would emit an invalid error message (#1633, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
* Fixed snomasks displaying in a nondeterministic order (#1669, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
|
||||
### Removed
|
||||
* Removed the `draft/resume-0.5` capability, and the associated `RESUME` and `BRB` commands (#1624)
|
||||
|
||||
### Internal
|
||||
* Optimized MySQL storage of direct messages (#1615)
|
||||
|
||||
## [2.6.1] - 2021-04-26
|
||||
|
||||
Oragono 2.6.1 is a bugfix release, fixing a security issue that is critical for some private server configurations. We regret the oversight.
|
||||
|
||||
The issue affects two classes of server configuration:
|
||||
|
||||
1. Private servers that use `server.password` (i.e., the `PASS` command) for protection. If `accounts.registration.allow-before-connect` is enabled, the `REGISTER` command can be used to bypass authentication. Affected operators should set this field to `false`, or upgrade to 2.6.1, which disallows the insecure configuration. (If the field does not appear in the configuration file, the configuration is secure since the value defaults to false when unset.)
|
||||
2. Private servers that use `accounts.require-sasl` for protection. If these servers do not additionally set `accounts.registration.enabled` to `false`, the `REGISTER` command can potentially be used to bypass authentication. Affected operators should set `accounts.registration.enabled` to false; this recommendation appeared in the operator manual but was not emphasized sufficiently. (Configurations that require SASL but allow open registration are potentially valid, e.g., in the case of public servers that require everyone to use a registered account; accordingly, Oragono 2.6.1 continues to permit such configurations.)
|
||||
|
||||
This release includes no changes to the config file format or the database.
|
||||
|
||||
Many thanks to [@ajaspers](https://github.com/ajaspers) for reporting the issue.
|
||||
|
||||
### Security
|
||||
* Fixed and documented potential authentication bypasses via the `REGISTER` command (#1634, thanks [@ajaspers](https://github.com/ajaspers)!)
|
||||
|
||||
## [2.6.0] - 2021-04-18
|
||||
|
||||
We're pleased to announce Oragono 2.6.0, a new stable release.
|
||||
|
||||
This release has some user-facing enhancements, but is primarily focused on fixing bugs and advancing the state of IRCv3 standardization (by publishing a release that implements the latest drafts). Some highlights:
|
||||
|
||||
* A new CHATHISTORY API for listing direct message conversations (#1592)
|
||||
* The latest proposal for IRC-over-websockets, which should be backwards-compatible with existing clients (#1558)
|
||||
* The latest specification for the bot usermode (`+B` in our implementation) (#1562)
|
||||
|
||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading.
|
||||
|
||||
This release includes no changes to the embedded database format. If you are using MySQL for history storage, it adds a new table; this change is backwards and forwards-compatible and does not require any manual intervention.
|
||||
|
||||
If you are using nginx as a reverse proxy for IRC-over-websockets, previous documentation did not recommend increasing `proxy_read_timeout`; the default value of `60s` is too low and can lead to user disconnections. The current recommended value is `proxy_read_timeout 600s;`; see the manual for an example configuration.
|
||||
|
||||
Many thanks to [@ajaspers](https://github.com/ajaspers) and [@Mikaela](https://github.com/Mikaela) for contributing patches, to [@aster1sk](https://github.com/aster1sk), [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@hhirtz](https://github.com/hhirtz), [@jlu5](https://github.com/jlu5), [@jwheare](https://github.com/jwheare), [@KoraggKnightWolf](https://github.com/KoraggKnightWolf), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@ProgVal](https://github.com/ProgVal), and [@szlend](https://github.com/szlend) for reporting issues and helping test, and to our translators for contributing translations.
|
||||
|
||||
### Config changes
|
||||
* Listeners now support multiple TLS certificates for use with SNI; see the manual for details (#875, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Added `server.compatibility.allow-truncation`, controlling whether the server accepts messages that are too long to be relayed intact; this value defaults to `true` when unset (#1577, #1586, thanks [@kylef](https://github.com/kylef)!)
|
||||
* Added new `snomasks` operator capability; operators must have either the `ban` or `snomasks` capability to subscribe to additional snomasks (#1176)
|
||||
|
||||
### Security
|
||||
* Fixed several edge cases where Oragono might relay invalid UTF8 despite the `UTF8ONLY` guarantee, or to a text-mode websocket client (#1575, #1596, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* All operator privilege checks now use the capabilities system, making it easier to define operators with restricted powers (#1176)
|
||||
* Adding and removing bans with `UBAN` now produces snomasks and audit loglines (#1518, thanks [@mogad0n](https://github.com/mogad0n)!)
|
||||
|
||||
### Fixed
|
||||
* Fixed an edge case in line buffering that could result in client disconnections (#1572, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* Upgraded buntdb, our embedded database library, fixing an edge case that could cause data corruption (#1603, thanks [@Mikaela](https://github.com/Mikaela), [@tidwall](https://github.com/tidwall)!)
|
||||
* Improved compatibility with the published `draft/register` specification (#1568, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* `433 ERR_NICKNAMEINUSE` is no longer sent when a fully connected ("registered") client fails to claim a reserved nickname, fixing a bad interaction with some client software (#1594, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* Fixed `znc.in/playback` commands causing client disconnections when history is disabled (#1552, thanks [@szlend](https://github.com/szlend)!)
|
||||
* Fixed syntactically invalid `696 ERR_INVALIDMODEPARAM` response for invalid channel keys (#1563, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* User-set nickserv settings now display as "enabled" instead of "mandatory" (#1544, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Improved error messages for some invalid configuration cases (#1559, thanks [@aster1sk](https://github.com/aster1sk)!)
|
||||
* Improved `CS TRANSFER` error messages (#1534, thanks burning!)
|
||||
* Handle panics caused when rehashing with SIGHUP (#1570)
|
||||
|
||||
### Changed
|
||||
* Registered channels will always appear in `/LIST` output, even with no members (#1507)
|
||||
* In the new recommended default configuration, Oragono will preemptively reject messages that are too long to be relayed to clients without truncation. This is controlled by the config variable `server.compatibility.allow-truncation`; this field defaults to `true` when unset, preserving the legacy behavior for older config files (#1577, #1586, thanks [@kylef](https://github.com/kylef)!)
|
||||
* Auto-away behavior now respects individual clients; the user is not considered away unless all clients are away or disconnected (#1531, thanks [@kylef](https://github.com/kylef)!)
|
||||
* Direct messages rejected due to the `+R` registered-only usermode now produce an error message (#1064, thanks [@KoraggKnightWolf](https://github.com/KoraggKnightWolf), [@ajaspers](https://github.com/ajaspers)!)
|
||||
* RELAYMSG identifiers now respect bans and mutes (#1502)
|
||||
* If end user message deletion is enabled, channel operators can now delete channel messages (#1565, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Halfops can change the channel topic (#1523)
|
||||
* Snomask add/remove syntax now matches other ircds more closely (#1074)
|
||||
* `CS OP` will regrant your channel `AMODE`, in case you removed it (#1516, #1307, thanks [@jlu5](https://github.com/jlu5)!)
|
||||
* User passwords may no longer begin with `:` (#1571)
|
||||
* Improved documentation of `CS AMODE` and `NS UNREGISTER` (#1524, #1545, thanks [@Mikaela](https://github.com/Mikaela)!)
|
||||
* Disabling history disables history-related CAPs (#1549)
|
||||
|
||||
### Added
|
||||
* Implemented the new [CHATHISTORY TARGETS](https://github.com/ircv3/ircv3-specifications/pull/450) API for listing direct message conversations (#1592, thanks [@emersion](https://github.com/emersion), [@hhirtz](https://github.com/hhirtz), [@jwheare](https://github.com/jwheare), [@kylef](https://github.com/kylef)!)
|
||||
* Implemented the new [IRC-over-websockets draft](https://github.com/ircv3/ircv3-specifications/pull/342), adding support for binary websockets and subprotocol negotiation (#1558, thanks [@jwheare](https://github.com/jwheare)!)
|
||||
* Implemented the new [bot mode spec](https://github.com/ircv3/ircv3-specifications/pull/439) (#1562)
|
||||
* Implemented the new [forward mode spec](https://github.com/ircv3/ircv3-specifications/pull/440) (#1612, thanks [@ProgVal](https://github.com/ProgVal)!)
|
||||
* `WARN NICK ACCOUNT_REQUIRED` is sent on failed attempts to claim a reserved nickname (#1594)
|
||||
* `NS CLIENTS LIST` displays enabled client capabilities (#1576)
|
||||
* `CS INFO` with no arguments lists your registered channels (#765)
|
||||
* `NS PASSWORD` is now accepted as an alias for `NS PASSWD` (#1547)
|
||||
|
||||
### Internal
|
||||
* Upgraded to Go 1.16 (#1510)
|
||||
|
||||
## [2.5.1] - 2021-02-02
|
||||
|
||||
@ -792,7 +1364,7 @@ Thanks to [slingamn](https://github.com/slingamn) for a lot of heavy lifting thi
|
||||
## [0.11.0] - 2018-04-15
|
||||
And v0.11.0 finally comes along! This release has been in the works for almost four months now, with an alpha and beta helping square away the issues.
|
||||
|
||||
We're adding a lot of features to improve debugging, better support international users, and make things better for network administrators. Among the new features, you can use the `LANGUAGE` command to set a custom server language (see our [CrowdIn](https://crowdin.com/project/oragono) to contribute), expose a debugging `pprof` endpoint, reserve nicknames with `NickServ`, and force email verification for new user accounts. On the improvements side we have a `CAP REQ` fix, and we now have a manual that contains a nice overview of Oragono's documentation.
|
||||
We're adding a lot of features to improve debugging, better support international users, and make things better for network administrators. Among the new features, you can use the `LANGUAGE` command to set a custom server language (see our [CrowdIn](https://crowdin.com/project/ergochat) to contribute), expose a debugging `pprof` endpoint, reserve nicknames with `NickServ`, and force email verification for new user accounts. On the improvements side we have a `CAP REQ` fix, and we now have a manual that contains a nice overview of Oragono's documentation.
|
||||
|
||||
If you have any trouble with this release, please let us know with an issue on our tracker, or by talking to us in `#oragono` on Freenode.
|
||||
|
||||
|
||||
@ -1,28 +1,48 @@
|
||||
# Developing Oragono
|
||||
# Developing Ergo
|
||||
|
||||
This is a guide to modifying Oragono's code. If you're just trying to run your own Oragono, or use one, you shouldn't need to worry about these issues.
|
||||
This is a guide to modifying Ergo's code. If you're just trying to run your own Ergo, or use one, you shouldn't need to worry about these issues.
|
||||
|
||||
|
||||
## Golang issues
|
||||
|
||||
You should use the [latest distribution of the Go language for your OS and architecture](https://golang.org/dl/). (If `uname -m` on your Raspberry Pi reports `armv7l`, use the `armv6l` distribution of Go; if it reports v8, you may be able to use the `arm64` distribution.)
|
||||
|
||||
Oragono vendors all its dependencies. Because of this, Oragono is self-contained and you should not need to fetch any dependencies with `go get`. Doing so is not recommended, since it may fetch incompatible versions of the dependencies.
|
||||
Ergo vendors all its dependencies. Because of this, Ergo is self-contained and you should not need to fetch any dependencies with `go get`. Doing so is not recommended, since it may fetch incompatible versions of the dependencies.
|
||||
|
||||
If you're upgrading the Go version used by Oragono, there are several places where it's hard-coded and must be changed:
|
||||
If you're upgrading the Go version used by Ergo, there are several places where it's hard-coded and must be changed:
|
||||
|
||||
1. `.travis.yml`, which controls the version that our CI test suite uses to build and test the code (e.g., for a PR)
|
||||
2. `Dockerfile`, which controls the version that the Oragono binaries in our Docker images are built with
|
||||
1. `.github/workflows/build.yml`, which controls the version that our CI test suite uses to build and test the code (e.g., for a PR)
|
||||
2. `Dockerfile`, which controls the version that the Ergo binaries in our Docker images are built with
|
||||
3. `go.mod`: this should be updated automatically by Go when you do module-related operations
|
||||
|
||||
|
||||
## Branches
|
||||
|
||||
The `master` branch should be kept relatively runnable. It might be a bit broken or contain some bad commits now and then, but the pre-release checks should weed those out before users see them.
|
||||
The recommended workflow for development is to create a new branch starting from the current `master`. Even though `master` is not recommended for production use, we strive to keep it in a usable state. Starting from `master` increases the likelihood that your patches will be accepted.
|
||||
|
||||
For either particularly broken or particularly WiP changes, we work on them in a `develop` branch. The normal branch naming is `develop+feature[.version]`. For example, when first developing 'cloaking', you may use the branch `develop+cloaks`. If you need to create a new branch to work on it (a second version of the implementation, for example), you could use `develop+cloaks.2`, and so on.
|
||||
Long-running feature branches that aren't ready for merge into `master` may be maintained under a `devel+` prefix, e.g. `devel+metadata` for a feature branch implementing the IRCv3 METADATA extension.
|
||||
|
||||
Develop branches are either used to work out implementation details in preperation for a cleaned-up version, for half-written ideas we want to continue persuing, or for stuff that we just don't want on `master` yet for whatever reason.
|
||||
|
||||
## Workflow
|
||||
|
||||
We have two test suites:
|
||||
|
||||
1. `make test`, which runs some relatively shallow unit tests, checks `go vet`, and does some other internal consistency checks
|
||||
1. `make irctest`, which runs the [irctest](https://github.com/ProgVal/irctest) integration test suite
|
||||
|
||||
Barring special circumstances, both must pass for a PR to be accepted. irctest will test the `ergo` binary visible on `$PATH`; make sure your development version is the one being tested. (If you have `~/go/bin` on your `$PATH`, a successful `make install` will accomplish this.)
|
||||
|
||||
The project style is [gofmt](https://go.dev/blog/gofmt); it is enforced by `make test`. You can fix any style issues automatically by running `make gofmt`.
|
||||
|
||||
|
||||
## Updating dependencies
|
||||
|
||||
Ergo vendors all dependencies using `go mod vendor`. To update a dependency, or add a new one:
|
||||
|
||||
1. `go get -v bazbat.com/path/to/dependency` ; this downloads the new dependency
|
||||
2. `go mod vendor` ; this writes the dependency's source files to the `vendor/` directory
|
||||
3. `git add go.mod go.sum vendor/` ; this stages all relevant changes to the vendor directory, including file deletions. Take care that spurious changes (such as editor swapfiles) aren't added.
|
||||
4. `git commit`
|
||||
|
||||
|
||||
## Releasing a new version
|
||||
@ -35,18 +55,22 @@ Develop branches are either used to work out implementation details in preperati
|
||||
1. Commit the new changelog and constants change.
|
||||
1. Tag the release with `git tag --sign v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number).
|
||||
1. Build binaries using `make release`
|
||||
1. Sign the checksums file with `gpg --sign --detach-sig --local-user <fingerprint>`
|
||||
1. Smoke-test a built binary locally
|
||||
1. Point of no return: `git push origin master --tags` (this publishes the tag; any fixes after this will require a new point release)
|
||||
1. Publish the release on GitHub (Releases -> "Draft a new release"); use the new tag, post the changelog entries, upload the binaries
|
||||
1. If it's a proper release (i.e. not an alpha/beta), merge the updates into the `stable` branch.
|
||||
1. Publish the release on GitHub (Releases -> "Draft a new release"); use the new tag, post the changelog entries, upload the binaries, the checksums file, and the signature of the checksums file
|
||||
1. Update the `irctest_stable` branch with the new changes (this may be a force push).
|
||||
1. If it's a production release (as opposed to a release candidate), update the `stable` branch with the new changes. (This may be a force push in the event that stable contained a backport. This is fine because all stable releases and release candidates are tagged.)
|
||||
1. Similarly, for a production release, update the `irctest_stable` branch (this is the branch used by upstream irctest to integration-test against Ergo).
|
||||
1. Make the appropriate announcements:
|
||||
* For a release candidate:
|
||||
1. the channel topic
|
||||
1. any operators who may be interested
|
||||
1. update the testnet
|
||||
* For a production release:
|
||||
1. everything applicable to a release candidate
|
||||
1. Twitter
|
||||
1. oragono.io/news
|
||||
1. ergo.chat/news
|
||||
1. ircv3.net support tables, if applicable
|
||||
1. other social media?
|
||||
|
||||
@ -60,7 +84,7 @@ Once it's built and released, you need to setup the new development version. To
|
||||
|
||||
```md
|
||||
## Unreleased
|
||||
New release of Oragono!
|
||||
New release of Ergo!
|
||||
|
||||
### Config Changes
|
||||
|
||||
@ -77,17 +101,6 @@ New release of Oragono!
|
||||
|
||||
|
||||
|
||||
## Fuzzing and Testing
|
||||
|
||||
Fuzzing can be useful. We don't have testing done inside the IRCd itself, but this fuzzer I've written works alright and has helped shake out various bugs: [irc_fuzz.py](https://gist.github.com/DanielOaks/63ae611039cdf591dfa4).
|
||||
|
||||
In addition, I've got the beginnings of a stress-tester here which is useful:
|
||||
https://github.com/DanielOaks/irc-stress-test
|
||||
|
||||
As well, there's a decent set of 'tests' here, which I like to run Oragono through now and then:
|
||||
https://github.com/DanielOaks/irctest
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
It's helpful to enable all loglines while developing. Here's how to configure this:
|
||||
@ -104,12 +117,12 @@ To debug a hang, the best thing to do is to get a stack trace. The easiest way t
|
||||
|
||||
$ kill -ABRT <procid>
|
||||
|
||||
This will kill Oragono and print out a stack trace for you to take a look at.
|
||||
This will kill Ergo and print out a stack trace for you to take a look at.
|
||||
|
||||
|
||||
## Concurrency design
|
||||
|
||||
Oragono involves a fair amount of shared state. Here are some of the main points:
|
||||
Ergo involves a fair amount of shared state. Here are some of the main points:
|
||||
|
||||
1. Each client has a separate goroutine that listens for incoming messages and synchronously processes them.
|
||||
1. All sends to clients are asynchronous; `client.Send` appends the message to a queue, which is then processed on a separate goroutine. It is always safe to call `client.Send`.
|
||||
@ -157,7 +170,7 @@ In addition, throughout most of the codebase, if a string is created using the b
|
||||
|
||||
## Updating Translations
|
||||
|
||||
We support translating server strings using [CrowdIn](https://crowdin.com/project/oragono)! To send updated source strings to CrowdIn, you should:
|
||||
We support translating server strings using [CrowdIn](https://crowdin.com/project/ergochat)! To send updated source strings to CrowdIn, you should:
|
||||
|
||||
1. `cd` to the base directory (the one this `DEVELOPING` file is in).
|
||||
2. Install the `pyyaml` and `docopt` deps using `pip3 install pyyamp docopt`.
|
||||
|
||||
50
Dockerfile
50
Dockerfile
@ -1,53 +1,43 @@
|
||||
## build Oragono
|
||||
FROM golang:1.15-alpine AS build-env
|
||||
## build ergo binary
|
||||
FROM docker.io/golang:1.25-alpine3.22 AS build-env
|
||||
|
||||
RUN apk add --no-cache git make curl sed
|
||||
RUN apk upgrade -U --force-refresh --no-cache && apk add --no-cache --purge --clean-protected -l -u make git
|
||||
|
||||
# copy oragono
|
||||
RUN mkdir -p /go/src/github.com/oragono/oragono
|
||||
WORKDIR /go/src/github.com/oragono/oragono
|
||||
ADD . /go/src/github.com/oragono/oragono/
|
||||
# copy ergo source
|
||||
WORKDIR /go/src/github.com/ergochat/ergo
|
||||
COPY . .
|
||||
|
||||
# modify default config file so that it doesn't die on IPv6
|
||||
# and so it can be exposed via 6667 by default
|
||||
run sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/oragono/oragono/default.yaml
|
||||
run sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/oragono/oragono/default.yaml
|
||||
RUN sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/ergochat/ergo/default.yaml && \
|
||||
sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/ergochat/ergo/default.yaml
|
||||
|
||||
# compile
|
||||
RUN make
|
||||
RUN make install
|
||||
|
||||
|
||||
|
||||
## run Oragono
|
||||
FROM alpine:3.9
|
||||
## build ergo container
|
||||
FROM docker.io/alpine:3.22
|
||||
|
||||
# metadata
|
||||
LABEL maintainer="daniel@danieloaks.net"
|
||||
LABEL description="Oragono is a modern, experimental IRC server written in Go"
|
||||
|
||||
# install latest updates and configure alpine
|
||||
RUN apk update
|
||||
RUN apk upgrade
|
||||
RUN mkdir /lib/modules
|
||||
LABEL maintainer="Daniel Oaks <daniel@danieloaks.net>,Daniel Thamdrup <dallemon@protonmail.com>" \
|
||||
description="Ergo is a modern, experimental IRC server written in Go"
|
||||
|
||||
# standard ports listened on
|
||||
EXPOSE 6667/tcp 6697/tcp
|
||||
|
||||
# oragono itself
|
||||
RUN mkdir -p /ircd-bin
|
||||
COPY --from=build-env /go/bin/oragono /ircd-bin
|
||||
COPY --from=build-env /go/src/github.com/oragono/oragono/languages /ircd-bin/languages/
|
||||
COPY --from=build-env /go/src/github.com/oragono/oragono/default.yaml /ircd-bin/default.yaml
|
||||
|
||||
COPY distrib/docker/run.sh /ircd-bin/run.sh
|
||||
RUN chmod +x /ircd-bin/run.sh
|
||||
# ergo itself
|
||||
COPY --from=build-env /go/bin/ergo \
|
||||
/go/src/github.com/ergochat/ergo/default.yaml \
|
||||
/go/src/github.com/ergochat/ergo/distrib/docker/run.sh \
|
||||
/ircd-bin/
|
||||
COPY --from=build-env /go/src/github.com/ergochat/ergo/languages /ircd-bin/languages/
|
||||
|
||||
# running volume holding config file, db, certs
|
||||
VOLUME /ircd
|
||||
WORKDIR /ircd
|
||||
|
||||
# default motd
|
||||
COPY --from=build-env /go/src/github.com/oragono/oragono/oragono.motd /ircd/oragono.motd
|
||||
COPY --from=build-env /go/src/github.com/ergochat/ergo/ergo.motd /ircd/ergo.motd
|
||||
|
||||
# launch
|
||||
ENTRYPOINT ["/ircd-bin/run.sh"]
|
||||
|
||||
49
Makefile
49
Makefile
@ -1,47 +1,48 @@
|
||||
.PHONY: all install build release capdefs test smoke gofmt irctest
|
||||
|
||||
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
||||
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
||||
|
||||
# disable linking against native libc / libpthread by default;
|
||||
# this can be overridden by passing CGO_ENABLED=1 to make
|
||||
export CGO_ENABLED ?= 0
|
||||
|
||||
capdef_file = ./irc/caps/defs.go
|
||||
|
||||
all: install
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -v -ldflags "-X main.commit=$(GIT_COMMIT)"
|
||||
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -v -ldflags "-X main.commit=$(GIT_COMMIT)"
|
||||
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
goreleaser --skip-publish --rm-dist
|
||||
goreleaser --skip=publish --clean
|
||||
|
||||
.PHONY: capdefs
|
||||
capdefs:
|
||||
python3 ./gencapdefs.py > ${capdef_file}
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||
cd irc && go test . && go vet .
|
||||
cd irc/caps && go test . && go vet .
|
||||
cd irc/cloaks && go test . && go vet .
|
||||
cd irc/connection_limits && go test . && go vet .
|
||||
cd irc/email && go test . && go vet .
|
||||
cd irc/flatip && go test . && go vet .
|
||||
cd irc/history && go test . && go vet .
|
||||
cd irc/isupport && go test . && go vet .
|
||||
cd irc/migrations && go test . && go vet .
|
||||
cd irc/modes && go test . && go vet .
|
||||
cd irc/mysql && go test . && go vet .
|
||||
cd irc/passwd && go test . && go vet .
|
||||
cd irc/utils && go test . && go vet .
|
||||
go test ./...
|
||||
go vet ./...
|
||||
./.check-gofmt.sh
|
||||
|
||||
smoke:
|
||||
oragono mkcerts --conf ./default.yaml || true
|
||||
oragono run --conf ./default.yaml --smoke
|
||||
.PHONY: smoke
|
||||
smoke: install
|
||||
ergo mkcerts --conf ./default.yaml || true
|
||||
ergo run --conf ./default.yaml --smoke
|
||||
|
||||
.PHONY: gofmt
|
||||
gofmt:
|
||||
./.check-gofmt.sh --fix
|
||||
|
||||
irctest:
|
||||
.PHONY: irctest
|
||||
irctest: install
|
||||
git submodule update --init
|
||||
cd irctest && make integration
|
||||
cd irctest && make ergo
|
||||
|
||||
35
README
35
README
@ -1,13 +1,12 @@
|
||||
|
||||
▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
|
||||
▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
|
||||
▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄
|
||||
▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌
|
||||
▀█▄▀▪.▀ ▀ ▀ ▀ ·▀▀▀▀ ▀█▄▀ ▀▀ █▪ ▀█▄▀▪
|
||||
|
||||
___ _ __ __ _ ___
|
||||
/ _ \ '__/ _` |/ _ \
|
||||
| __/ | | (_| | (_) |
|
||||
\___|_| \__, |\___/
|
||||
__/ |
|
||||
|___/
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
Oragono is a modern IRC server written in Go. Its core design principles are:
|
||||
Ergo is a modern IRC server written in Go. Its core design principles are:
|
||||
|
||||
* Being simple to set up and use
|
||||
* Combining the features of an ircd, a services framework, and a bouncer:
|
||||
@ -15,11 +14,11 @@ Oragono is a modern IRC server written in Go. Its core design principles are:
|
||||
* History storage
|
||||
* Bouncer functionality
|
||||
* Bleeding-edge IRCv3 support
|
||||
* Highly customizable via a rehashable (runtime-reloadable) YAML config
|
||||
* High customizability via a rehashable (i.e., reloadable at runtime) YAML config
|
||||
|
||||
https://oragono.io/
|
||||
https://github.com/oragono/oragono
|
||||
#oragono on Freenode
|
||||
https://ergo.chat/
|
||||
https://github.com/ergochat/ergo
|
||||
#ergo on irc.ergo.chat or irc.libera.chat
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
@ -34,23 +33,23 @@ Modify the config file as needed (the recommendations at the top may be helpful)
|
||||
|
||||
To generate passwords for opers and connect passwords, you can use this command:
|
||||
|
||||
$ oragono genpasswd
|
||||
$ ./ergo genpasswd
|
||||
|
||||
If you need to generate self-signed TLS certificates, use this command:
|
||||
|
||||
$ oragono mkcerts
|
||||
$ ./ergo mkcerts
|
||||
|
||||
You are now ready to start Oragono!
|
||||
You are now ready to start Ergo!
|
||||
|
||||
$ oragono run
|
||||
$ ./ergo run
|
||||
|
||||
For further instructions, consult the manual. A copy of the manual should be
|
||||
included in your release under `docs/MANUAL.md`. Or you can view it on the
|
||||
Web: https://oragono.io/manual.html
|
||||
Web: https://ergo.chat/manual.html
|
||||
|
||||
=== Updating ===
|
||||
|
||||
If you're updating from a previous version of Oragono, check out the CHANGELOG for a list
|
||||
If you're updating from a previous version of Ergo, check out the CHANGELOG for a list
|
||||
of important changes you'll want to take a look at. The change log details config changes,
|
||||
fixes, new features and anything else you'll want to be aware of!
|
||||
|
||||
|
||||
73
README.md
73
README.md
@ -1,23 +1,22 @@
|
||||

|
||||

|
||||
|
||||
Oragono is a modern IRC server written in Go. Its core design principles are:
|
||||
Ergo (formerly known as Oragono) is a modern IRC server written in Go. Its core design principles are:
|
||||
|
||||
* Being simple to set up and use
|
||||
* Combining the features of an ircd, a services framework, and a bouncer (integrated account management, history storage, and bouncer functionality)
|
||||
* Bleeding-edge [IRCv3 support](https://ircv3.net/software/servers.html), suitable for use as an IRCv3 reference implementation
|
||||
* Highly customizable via a rehashable (i.e., reloadable at runtime) YAML config
|
||||
* High customizability via a rehashable (i.e., reloadable at runtime) YAML config
|
||||
|
||||
Oragono is a fork of the [Ergonomadic](https://github.com/jlatt/ergonomadic) IRC daemon <3
|
||||
Ergo is a fork of the [Ergonomadic](https://github.com/jlatt/ergonomadic) IRC daemon <3
|
||||
|
||||
---
|
||||
|
||||
[](https://goreportcard.com/report/github.com/oragono/oragono)
|
||||
[](https://travis-ci.com/oragono/oragono)
|
||||
[](https://github.com/oragono/oragono/releases/latest)
|
||||
[](https://www.irccloud.com/invite?channel=%23oragono&hostname=irc.freenode.net&port=6697&ssl=1)
|
||||
[](https://crowdin.com/project/oragono)
|
||||
[](https://goreportcard.com/report/github.com/ergochat/ergo)
|
||||
[](https://github.com/ergochat/ergo/actions/workflows/build.yml)
|
||||
[](https://github.com/ergochat/ergo/releases/latest)
|
||||
[](https://crowdin.com/project/ergochat)
|
||||
|
||||
If you want to take a look at a running Oragono instance or test some client code, feel free to play with [testnet.oragono.io](https://testnet.oragono.io/) (TLS on port 6697 or plaintext on port 6667).
|
||||
If you want to take a look at a running Ergo instance or test some client code, feel free to play with [testnet.ergo.chat](https://testnet.ergo.chat/) (TLS on port 6697 or plaintext on port 6667).
|
||||
|
||||
---
|
||||
|
||||
@ -26,68 +25,72 @@ If you want to take a look at a running Oragono instance or test some client cod
|
||||
|
||||
* integrated services: NickServ for user accounts, ChanServ for channel registration, and HostServ for vanity hosts
|
||||
* bouncer-like features: storing and replaying history, allowing multiple clients to use the same nickname
|
||||
* UTF-8 nick and channel names with rfc7613 (PRECIS)
|
||||
* native TLS/SSL support, including support for client certificates
|
||||
* [IRCv3 support](https://ircv3.net/software/servers.html)
|
||||
* [yaml](https://yaml.org/) configuration
|
||||
* updating server config and TLS certificates on-the-fly (rehashing)
|
||||
* SASL authentication
|
||||
* LDAP support
|
||||
* supports [multiple languages](https://crowdin.com/project/oragono) (you can also set a default language for your network)
|
||||
* [LDAP support](https://github.com/ergochat/ergo-ldap)
|
||||
* supports [multiple languages](https://crowdin.com/project/ergochat) (you can also set a default language for your network)
|
||||
* optional support for UTF-8 nick and channel names with RFC 8265 (PRECIS)
|
||||
* advanced security and privacy features (support for requiring SASL for all logins, cloaking IPs, and running as a Tor hidden service)
|
||||
* an extensible privilege system for IRC operators
|
||||
* ident lookups for usernames
|
||||
* automated client connection limits
|
||||
* passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto)
|
||||
* `UBAN`, a unified ban system that can target IPs, networks, masks, and registered accounts (`KLINE` and `DLINE` are also supported)
|
||||
* [IRCv3 support](https://ircv3.net/software/servers.html)
|
||||
* a focus on developing with [specifications](https://oragono.io/specs.html)
|
||||
* a focus on developing with [specifications](https://ergo.chat/specs.html)
|
||||
|
||||
For more detailed information on Ergo's functionality, see:
|
||||
|
||||
* [MANUAL.md, the operator manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md)
|
||||
* [USERGUIDE.md, the guide for end users](https://github.com/ergochat/ergo/blob/stable/docs/USERGUIDE.md)
|
||||
|
||||
## Quick start guide
|
||||
|
||||
Download the latest release from this page: https://github.com/oragono/oragono/releases/latest
|
||||
Download the latest release from this page: https://github.com/ergochat/ergo/releases/latest
|
||||
|
||||
Extract it into a folder, then run the following commands:
|
||||
|
||||
```sh
|
||||
cp default.yaml ircd.yaml
|
||||
vim ircd.yaml # modify the config file to your liking
|
||||
oragono mkcerts
|
||||
oragono run # server should be ready to go!
|
||||
vim ircd.yaml # modify the config file to your liking
|
||||
./ergo mkcerts
|
||||
./ergo run # server should be ready to go!
|
||||
```
|
||||
|
||||
**Note:** See the [productionizing guide in our manual](https://github.com/oragono/oragono/blob/master/docs/MANUAL.md#productionizing) for recommendations on how to run a production network, including obtaining valid TLS certificates.
|
||||
**Note:** See the [productionizing guide in our manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md#productionizing-with-systemd) for recommendations on how to run a production network, including obtaining valid TLS certificates.
|
||||
|
||||
### Platform Packages
|
||||
|
||||
Some platforms/distros also have Oragono packages maintained for them:
|
||||
Some platforms/distros also have Ergo packages maintained for them:
|
||||
|
||||
* Arch Linux [AUR](https://aur.archlinux.org/packages/oragono/) - Maintained by [Sean Enck (@enckse)](https://github.com/enckse).
|
||||
* Arch Linux [AUR](https://aur.archlinux.org/packages/ergochat/) - Maintained by [Jason Papakostas (@vith)](https://github.com/vith).
|
||||
* [Gentoo Linux](https://packages.gentoo.org/packages/net-irc/ergo) - Maintained by [Sam James (@thesamesam)](https://github.com/thesamesam).
|
||||
|
||||
### Using Docker
|
||||
|
||||
A Dockerfile and example docker-compose recipe are available in the `distrib/docker` directory. Oragono is automatically published
|
||||
to Docker Hub at [oragono/oragono](https://hub.docker.com/r/oragono/oragono). For more information, see the distrib/docker
|
||||
[README file](https://github.com/oragono/oragono/blob/master/distrib/docker/README.md).
|
||||
A Dockerfile and example docker-compose recipe are available in the `distrib/docker` directory. Ergo is automatically published
|
||||
to the GitHub Container Registry at [ghcr.io/ergochat/ergo](https://ghcr.io/ergochat/ergo). For more information, see the distrib/docker
|
||||
[README file](https://github.com/ergochat/ergo/blob/master/distrib/docker/README.md).
|
||||
|
||||
### From Source
|
||||
|
||||
You can also install this repo and use that instead! However, keep some things in mind if you go that way:
|
||||
You can also clone this repository and build from source. Typical deployments should use the `stable` branch, which points to the latest stable release. In general, `stable` should coincide with the latest published tag that is not designated as a beta or release candidate (for example, `v2.7.0-rc1` was an unstable release candidate and `v2.7.0` was the corresponding stable release), so you can also identify the latest stable release tag on the [releases page](https://github.com/ergochat/ergo/releases) and build that.
|
||||
|
||||
`devel` branches are intentionally unstable, containing fixes that may not work, and they may be rebased or reworked extensively.
|
||||
The `master` branch is not recommended for production use since it may contain bugs, and because the forwards compatibility guarantees for the config file and the database that apply to releases do not apply to master. That is to say, running master may result in changes to your database that end up being incompatible with future versions of Ergo.
|
||||
|
||||
The `master` branch _should_ usually be stable, but may contain database changes that either have not been finalised or not had database upgrade code written yet. Don't run `master` on a live production network.
|
||||
|
||||
The `stable` branch contains the latest release. You can run this for a production version without any trouble.
|
||||
For information on contributing to Ergo, see [DEVELOPING.md](https://github.com/ergochat/ergo/blob/master/DEVELOPING.md).
|
||||
|
||||
#### Building
|
||||
|
||||
You'll need an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). Once you have that, just clone the repository and run `make build`. If everything goes well, you should now have an executable named `oragono` in the base directory of the project.
|
||||
You'll need an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). Once that's installed (check the output of `go version`), just check out your desired branch or tag and run `make`. This will produce an executable binary named `ergo` in the base directory of the project. (Ergo vendors all its dependencies, so you will not need to fetch any dependencies remotely.)
|
||||
|
||||
## Configuration
|
||||
|
||||
The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes.
|
||||
|
||||
You can use the `--conf` parameter when launching Oragono to control where it looks for the config file. For instance: `oragono run --conf /path/to/ircd.yaml`. The configuration file also stores where the log, database, certificate, and other files are opened. Normally, all these files use relative paths, but you can change them to be absolute (such as `/var/log/ircd.log`) when running Oragono as a service.
|
||||
You can use the `--conf` parameter when launching Ergo to control where it looks for the config file. For instance: `ergo run --conf /path/to/ircd.yaml`. The configuration file also stores where the log, database, certificate, and other files are opened. Normally, all these files use relative paths, but you can change them to be absolute (such as `/var/log/ircd.log`) when running Ergo as a service.
|
||||
|
||||
### Logs
|
||||
|
||||
@ -98,14 +101,14 @@ By default, logs go to stderr only. They can be configured to go to a file, or y
|
||||
Passwords (for both `PASS` and oper logins) are stored using bcrypt. To generate encrypted strings for use in the config, use the `genpasswd` subcommand as such:
|
||||
|
||||
```sh
|
||||
oragono genpasswd
|
||||
ergo genpasswd
|
||||
```
|
||||
|
||||
With this, you receive a blob of text which you can plug into your configuration file.
|
||||
|
||||
### Nickname and channel registration
|
||||
|
||||
Oragono relies heavily on user accounts to enable its distinctive features (such as allowing multiple clients per nickname). As a user, you can register your current nickname as an account using `/msg NickServ register <password>`. Once you have done so, you should [enable SASL in your clients](https://freenode.net/kb/answer/sasl), ensuring that you will be automatically logged into your account on each connection. This will prevent [problems claiming your registered nickname](https://github.com/oragono/oragono/blob/master/docs/MANUAL.md#nick-equals-account).
|
||||
Ergo relies heavily on user accounts to enable its distinctive features (such as allowing multiple clients per nickname). As a user, you can register your current nickname as an account using `/msg NickServ register <password>`. Once you have done so, you should [enable SASL in your clients](https://libera.chat/guides/sasl), ensuring that you will be automatically logged into your account on each connection. This will prevent [problems claiming your registered nickname](https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md#nick-equals-account).
|
||||
|
||||
Once you have registered your nickname, you can use it to register channels:
|
||||
|
||||
@ -121,4 +124,4 @@ After this, your channel will remember the fact that you're the owner, the topic
|
||||
* Edmund Huber (2014-2015)
|
||||
* Daniel Oaks (2016-present)
|
||||
* Shivaram Lingamneni (2017-present)
|
||||
* [Many other contributors and friends of the project <3](https://github.com/oragono/oragono/blob/master/CHANGELOG.md)
|
||||
* [Many other contributors and friends of the project <3](https://github.com/ergochat/ergo/blob/master/CHANGELOG.md)
|
||||
|
||||
364
default.yaml
364
default.yaml
@ -1,10 +1,10 @@
|
||||
# This is the default config file for Oragono.
|
||||
# This is the default config file for Ergo.
|
||||
# It contains recommended defaults for all settings, including some behaviors
|
||||
# that differ from conventional ircd+services setups. See traditional.yaml
|
||||
# for a config with more "mainstream" behavior.
|
||||
#
|
||||
# If you are setting up a new oragono server, you should copy this file
|
||||
# to a new one named 'ircd.yaml', then read the whole file to see which
|
||||
# If you are setting up a new Ergo server, you should copy this file
|
||||
# to a new one named 'ircd.yaml', then look through the file to see which
|
||||
# settings you want to customize. If you don't understand a setting, or
|
||||
# aren't sure what behavior you want, most of the defaults are fine
|
||||
# to start with (you can change them later, even on a running server).
|
||||
@ -25,12 +25,12 @@
|
||||
# network configuration
|
||||
network:
|
||||
# name of the network
|
||||
name: OragonoTest
|
||||
name: ErgoTest
|
||||
|
||||
# server configuration
|
||||
server:
|
||||
# server name
|
||||
name: oragono.test
|
||||
name: ergo.test
|
||||
|
||||
# addresses to listen on
|
||||
listeners:
|
||||
@ -49,6 +49,8 @@ server:
|
||||
|
||||
# The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces:
|
||||
":6697":
|
||||
# this is a standard TLS configuration with a single certificate;
|
||||
# see the manual for instructions on how to configure SNI
|
||||
tls:
|
||||
cert: fullchain.pem
|
||||
key: privkey.pem
|
||||
@ -56,14 +58,16 @@ server:
|
||||
# always send a PROXY protocol header ahead of the connection. See the
|
||||
# manual ("Reverse proxies") for more details.
|
||||
proxy: false
|
||||
# set the minimum TLS version:
|
||||
min-tls-version: 1.2
|
||||
|
||||
# Example of a Unix domain socket for proxying:
|
||||
# "/tmp/oragono_sock":
|
||||
# "/tmp/ergo_sock":
|
||||
|
||||
# Example of a Tor listener: any connection that comes in on this listener will
|
||||
# be considered a Tor connection. It is strongly recommended that this listener
|
||||
# *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain:
|
||||
# "/hidden_service_sockets/oragono_tor_sock":
|
||||
# "/hidden_service_sockets/ergo_tor_sock":
|
||||
# tor: true
|
||||
|
||||
# Example of a WebSocket listener:
|
||||
@ -96,6 +100,7 @@ server:
|
||||
max-connections-per-duration: 64
|
||||
|
||||
# strict transport security, to get clients to automagically use TLS
|
||||
# (irrelevant in the recommended configuration, with no public plaintext listener)
|
||||
sts:
|
||||
# whether to advertise STS
|
||||
#
|
||||
@ -116,26 +121,27 @@ server:
|
||||
|
||||
websockets:
|
||||
# Restrict the origin of WebSocket connections by matching the "Origin" HTTP
|
||||
# header. This setting causes oragono to reject websocket connections unless
|
||||
# header. This setting causes ergo to reject websocket connections unless
|
||||
# they originate from a page on one of the whitelisted websites in this list.
|
||||
# This prevents malicious websites from making their visitors connect to your
|
||||
# oragono instance without their knowledge. An empty list means there are no
|
||||
# ergo instance without their knowledge. An empty list means there are no
|
||||
# restrictions.
|
||||
allowed-origins:
|
||||
# - "https://oragono.io"
|
||||
# - "https://*.oragono.io"
|
||||
# - "https://ergo.chat"
|
||||
# - "https://*.ergo.chat"
|
||||
|
||||
# casemapping controls what kinds of strings are permitted as identifiers (nicknames,
|
||||
# channel names, account names, etc.), and how they are normalized for case.
|
||||
# with the recommended default of 'precis', UTF8 identifiers that are "sane"
|
||||
# (according to RFC 8265) are allowed, and the server additionally tries to protect
|
||||
# against confusable characters ("homoglyph attacks").
|
||||
# the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive',
|
||||
# which allows identifiers to contain unusual characters like emoji, but makes users
|
||||
# vulnerable to homoglyph attacks. unless you're really confident in your decision,
|
||||
# we recommend leaving this value at its default (changing it once the network is
|
||||
# already up and running is problematic).
|
||||
casemapping: "precis"
|
||||
# the recommended default is 'ascii' (traditional ASCII-only identifiers).
|
||||
# the other options are 'precis', which allows UTF8 identifiers that are "sane"
|
||||
# (according to UFC 8265), with additional mitigations for homoglyph attacks,
|
||||
# 'permissive', which allows identifiers containing unusual characters like
|
||||
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential
|
||||
# client compatibility problems, and the legacy mappings 'rfc1459' and
|
||||
# 'rfc1459-strict'. we recommend leaving this value at its default;
|
||||
# however, note that changing it once the network is already up and running is
|
||||
# problematic.
|
||||
casemapping: "ascii"
|
||||
|
||||
# enforce-utf8 controls whether the server will preemptively discard non-UTF8
|
||||
# messages (since they cannot be relayed to websocket clients), or will allow
|
||||
@ -160,18 +166,31 @@ server:
|
||||
# the value must begin with a '~' character. comment out / omit to disable:
|
||||
coerce-ident: '~u'
|
||||
|
||||
# password to login to the server
|
||||
# generated using "oragono genpasswd"
|
||||
#password: ""
|
||||
# 'password' allows you to require a global, shared password (the IRC `PASS` command)
|
||||
# to connect to the server. for operator passwords, see the `opers` section of the
|
||||
# config. for a more secure way to create a private server, see the `require-sasl`
|
||||
# section. you must hash the password with `ergo genpasswd`, then enter the hash here:
|
||||
#password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
|
||||
|
||||
# motd filename
|
||||
# if you change the motd, you should move it to ircd.motd
|
||||
motd: oragono.motd
|
||||
motd: ergo.motd
|
||||
|
||||
# motd formatting codes
|
||||
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
||||
motd-formatting: true
|
||||
|
||||
# idle timeouts for inactive clients
|
||||
idle-timeouts:
|
||||
# give the client this long to complete connection registration (i.e. the initial
|
||||
# IRC handshake, including capability negotiation and SASL)
|
||||
registration: 60s
|
||||
# if the client hasn't sent anything for this long, send them a PING
|
||||
ping: 1m30s
|
||||
# if the client hasn't sent anything for this long (including the PONG to the
|
||||
# above PING), disconnect them
|
||||
disconnect: 2m30s
|
||||
|
||||
# relaying using the RELAYMSG command
|
||||
relaymsg:
|
||||
# is relaymsg enabled at all?
|
||||
@ -202,7 +221,7 @@ server:
|
||||
# (comment this out to use passwords only)
|
||||
certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
||||
|
||||
# password the gateway uses to connect, made with oragono genpasswd
|
||||
# password the gateway uses to connect, made with `ergo genpasswd`
|
||||
password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde"
|
||||
|
||||
# IPs/CIDRs that can use this webirc command
|
||||
@ -212,9 +231,9 @@ server:
|
||||
# - "192.168.1.1"
|
||||
# - "192.168.10.1/24"
|
||||
|
||||
# allow use of the RESUME extension over plaintext connections:
|
||||
# do not enable this unless the ircd is only accessible over internal networks
|
||||
allow-plaintext-resume: false
|
||||
# whether to accept the hostname parameter on the WEBIRC line as the IRC hostname
|
||||
# (the default/recommended Ergo configuration will use cloaks instead)
|
||||
accept-hostname: false
|
||||
|
||||
# maximum length of clients' sendQ in bytes
|
||||
# this should be big enough to hold bursts of channel/direct messages
|
||||
@ -224,7 +243,7 @@ server:
|
||||
compatibility:
|
||||
# many clients require that the final parameter of certain messages be an
|
||||
# RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is
|
||||
# actually required. this forces Oragono to send those parameters
|
||||
# actually required. this forces Ergo to send those parameters
|
||||
# as trailings. this is recommended unless you're testing clients for conformance;
|
||||
# defaults to true when unset for that reason.
|
||||
force-trailing: true
|
||||
@ -235,6 +254,13 @@ server:
|
||||
# this works around that bug, allowing them to use SASL.
|
||||
send-unprefixed-sasl: true
|
||||
|
||||
# traditionally, IRC servers will truncate and send messages that are
|
||||
# too long to be relayed intact. this behavior can be disabled by setting
|
||||
# allow-truncation to false, in which case Ergo will reject the message
|
||||
# and return an error to the client. (note that this option defaults to true
|
||||
# when unset.)
|
||||
allow-truncation: false
|
||||
|
||||
# IP-based DoS protection
|
||||
ip-limits:
|
||||
# whether to limit the total number of concurrent connections per IP/CIDR
|
||||
@ -294,11 +320,14 @@ server:
|
||||
kill-timeout: 1s
|
||||
# how many scripts are allowed to run at once? 0 for no limit:
|
||||
max-concurrency: 64
|
||||
# if true, only check anonymous connections (not logged into an account)
|
||||
# at the very end of the handshake:
|
||||
exempt-sasl: false
|
||||
|
||||
# IP cloaking hides users' IP addresses from other users and from channel admins
|
||||
# (but not from server admins), while still allowing channel admins to ban
|
||||
# offending IP addresses or networks. In place of hostnames derived from reverse
|
||||
# DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are
|
||||
# DNS, users see fake domain names like pwbs2ui4377257x8.irc. These names are
|
||||
# generated deterministically from the underlying IP address, but if the underlying
|
||||
# IP is not already known, it is infeasible to recover it from the cloaked name.
|
||||
# If you disable this, you should probably enable lookup-hostnames in its place.
|
||||
@ -340,15 +369,39 @@ server:
|
||||
secure-nets:
|
||||
# - "10.0.0.0/8"
|
||||
|
||||
# oragono will write files to disk under certain circumstances, e.g.,
|
||||
# allow attempts to OPER with a password at most this often. defaults to
|
||||
# 10 seconds when unset.
|
||||
oper-throttle: 10s
|
||||
|
||||
# Ergo will write files to disk under certain circumstances, e.g.,
|
||||
# CPU profiling or data export. by default, these files will be written
|
||||
# to the working directory. set this to customize:
|
||||
#output-path: "/home/oragono/out"
|
||||
#output-path: "/home/ergo/out"
|
||||
|
||||
# the hostname used by "services", e.g., NickServ, defaults to "localhost",
|
||||
# e.g., `NickServ!NickServ@localhost`. uncomment this to override:
|
||||
#override-services-hostname: "example.network"
|
||||
|
||||
# in a "closed-loop" system where you control the server and all the clients,
|
||||
# you may want to increase the maximum (non-tag) length of an IRC line from
|
||||
# the default value of 512. DO NOT change this on a public server:
|
||||
#max-line-len: 512
|
||||
|
||||
# send all 0's as the LUSERS (user counts) output to non-operators; potentially useful
|
||||
# if you don't want to publicize how popular the server is
|
||||
suppress-lusers: false
|
||||
|
||||
# publish additional key-value pairs in ISUPPORT (the 005 numeric).
|
||||
# keys that collide with a key published by Ergo will be silently ignored.
|
||||
additional-isupport:
|
||||
#"draft/FILEHOST": "https://example.com/filehost"
|
||||
#"draft/bazbat": "" # empty string means no value
|
||||
|
||||
# optionally map command alias names to existing ergo commands. most deployments
|
||||
# should ignore this.
|
||||
#command-aliases:
|
||||
#"UMGEBUNG": "AMBIANCE"
|
||||
|
||||
# account options
|
||||
accounts:
|
||||
# is account authentication enabled, i.e., can users log into existing accounts?
|
||||
@ -384,6 +437,10 @@ accounts:
|
||||
sender: "admin@my.network"
|
||||
require-tls: true
|
||||
helo-domain: "my.network" # defaults to server name if unset
|
||||
# set to `tcp4` to force sending over IPv4, `tcp6` to force IPv6:
|
||||
# protocol: "tcp4"
|
||||
# set to force a specific source/local IPv4 or IPv6 address:
|
||||
# local-address: "1.2.3.4"
|
||||
# options to enable DKIM signing of outgoing emails (recommended, but
|
||||
# requires creating a DNS entry for the public key):
|
||||
# dkim:
|
||||
@ -396,8 +453,23 @@ accounts:
|
||||
# port: 25
|
||||
# username: "admin"
|
||||
# password: "hunter2"
|
||||
blacklist-regexes:
|
||||
# - ".*@mailinator.com"
|
||||
# implicit-tls: false # TLS from the first byte, typically on port 465
|
||||
# addresses that are not accepted for registration:
|
||||
address-blacklist:
|
||||
# - "*@mailinator.com"
|
||||
address-blacklist-syntax: "glob" # change to "regex" for regular expressions
|
||||
# file of newline-delimited address blacklist entries (no enclosing quotes)
|
||||
# in the above syntax (i.e. either globs or regexes). supersedes
|
||||
# address-blacklist if set:
|
||||
# address-blacklist-file: "/path/to/address-blacklist-file"
|
||||
timeout: 60s
|
||||
# email-based password reset:
|
||||
password-reset:
|
||||
enabled: false
|
||||
# time before we allow resending the email
|
||||
cooldown: 1h
|
||||
# time for which a password reset code is valid
|
||||
timeout: 1d
|
||||
|
||||
# throttle account login attempts (to prevent either password guessing, or DoS
|
||||
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
||||
@ -421,10 +493,17 @@ accounts:
|
||||
# this is useful for compatibility with old clients that don't support SASL
|
||||
login-via-pass-command: true
|
||||
|
||||
# advertise the SCRAM-SHA-256 authentication method. set to false in case of
|
||||
# compatibility issues with certain clients:
|
||||
advertise-scram: true
|
||||
|
||||
# require-sasl controls whether clients are required to have accounts
|
||||
# (and sign into them using SASL) to connect to the server
|
||||
require-sasl:
|
||||
# if this is enabled, all clients must authenticate with SASL while connecting
|
||||
# if this is enabled, all clients must authenticate with SASL while connecting.
|
||||
# WARNING: for a private server, you MUST set accounts.registration.enabled
|
||||
# to false as well, in order to prevent non-administrators from registering
|
||||
# accounts.
|
||||
enabled: false
|
||||
|
||||
# IPs/CIDRs which are exempted from the account requirement
|
||||
@ -438,7 +517,9 @@ accounts:
|
||||
enabled: true
|
||||
|
||||
# how many nicknames, in addition to the account name, can be reserved?
|
||||
additional-nick-limit: 2
|
||||
# (note that additional nicks are unusable under force-nick-equals-account
|
||||
# or if the client is always-on)
|
||||
additional-nick-limit: 0
|
||||
|
||||
# method describes how nickname reservation is handled
|
||||
# strict: users must already be logged in to their account (via
|
||||
@ -456,7 +537,7 @@ accounts:
|
||||
# 1. these nicknames cannot be registered or reserved
|
||||
# 2. if a client is automatically renamed by the server,
|
||||
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
|
||||
# 3. if enforce-guest-format (see below) is enabled, clients without
|
||||
# 3. if force-guest-format (see below) is enabled, clients without
|
||||
# a registered account will have this template applied to their
|
||||
# nicknames (e.g., 'katie' will become 'Guest-katie')
|
||||
guest-nickname-format: "Guest-*"
|
||||
@ -478,7 +559,7 @@ accounts:
|
||||
# nickname after the initial connection is complete
|
||||
forbid-anonymous-nick-changes: false
|
||||
|
||||
# multiclient controls whether oragono allows multiple connections to
|
||||
# multiclient controls whether Ergo allows multiple connections to
|
||||
# attach to the same client/nickname identity; this is part of the
|
||||
# functionality traditionally provided by a bouncer like ZNC
|
||||
multiclient:
|
||||
@ -541,12 +622,47 @@ accounts:
|
||||
# how many scripts are allowed to run at once? 0 for no limit:
|
||||
max-concurrency: 64
|
||||
|
||||
# support for login via OAuth2 bearer tokens
|
||||
oauth2:
|
||||
enabled: false
|
||||
# should we automatically create users on presentation of a valid token?
|
||||
autocreate: true
|
||||
# enable this to use auth-script for validation:
|
||||
auth-script: false
|
||||
introspection-url: "https://example.com/api/oidc/introspection"
|
||||
introspection-timeout: 10s
|
||||
# omit for auth method `none`; required for auth method `client_secret_basic`:
|
||||
client-id: "ergo"
|
||||
client-secret: "4TA0I7mJ3fUUcW05KJiODg"
|
||||
|
||||
# support for login via JWT bearer tokens
|
||||
jwt-auth:
|
||||
enabled: false
|
||||
# should we automatically create users on presentation of a valid token?
|
||||
autocreate: true
|
||||
# any of these token definitions can be accepted, allowing for key rotation
|
||||
tokens:
|
||||
-
|
||||
algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519)
|
||||
# hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys;
|
||||
# either way, the key can be specified either as a YAML string:
|
||||
key: "nANiZ1De4v6WnltCHN2H7Q"
|
||||
# or as a path to the file containing the key:
|
||||
#key-file: "jwt_pubkey.pem"
|
||||
# list of JWT claim names to search for the user's account name (make sure the format
|
||||
# is what you expect, especially if using "sub"):
|
||||
account-claims: ["preferred_username"]
|
||||
# if a claim is formatted as an email address, require it to have the following domain,
|
||||
# and then strip off the domain and use the local-part as the account name:
|
||||
#strip-domain: "example.com"
|
||||
|
||||
# channel options
|
||||
channels:
|
||||
# modes that are set when new channels are created
|
||||
# +n is no-external-messages and +t is op-only-topic
|
||||
# +n is no-external-messages, +t is op-only-topic,
|
||||
# +C is no CTCPs (besides ACTION)
|
||||
# see /QUOTE HELP cmodes for more channel modes
|
||||
default-modes: +nt
|
||||
default-modes: +ntC
|
||||
|
||||
# how many channels can a client be in at once?
|
||||
max-channels-per-client: 100
|
||||
@ -575,7 +691,18 @@ channels:
|
||||
# (0 or omit for no expiration):
|
||||
invite-expiration: 24h
|
||||
|
||||
# operator classes
|
||||
# channels that new clients will automatically join. this should be used with
|
||||
# caution, since traditional IRC users will likely view it as an antifeature.
|
||||
# it may be useful in small community networks that have a single "primary" channel:
|
||||
#auto-join:
|
||||
# - "#lounge"
|
||||
|
||||
# operator classes:
|
||||
# an operator has a single "class" (defining a privilege level), which can include
|
||||
# multiple "capabilities" (defining privileged actions they can take). all
|
||||
# currently available operator capabilities are associated with either the
|
||||
# 'chat-moderator' class (less privileged) or the 'server-admin' class (full
|
||||
# privileges) below: you can mix and match to create new classes.
|
||||
oper-classes:
|
||||
# chat moderator: can ban/unban users from the server, join channels,
|
||||
# fix mode issues and sort out vhosts.
|
||||
@ -585,14 +712,15 @@ oper-classes:
|
||||
|
||||
# capability names
|
||||
capabilities:
|
||||
- "kill"
|
||||
- "ban"
|
||||
- "nofakelag"
|
||||
- "roleplay"
|
||||
- "relaymsg"
|
||||
- "vhosts"
|
||||
- "sajoin"
|
||||
- "samode"
|
||||
- "kill" # disconnect user sessions
|
||||
- "ban" # ban IPs, CIDRs, NUH masks, and suspend accounts (UBAN / DLINE / KLINE)
|
||||
- "nofakelag" # exempted from "fakelag" restrictions on rate of message sending
|
||||
- "relaymsg" # use RELAYMSG in any channel (see the `relaymsg` config block)
|
||||
- "vhosts" # add and remove vhosts from users
|
||||
- "sajoin" # join arbitrary channels, including private channels
|
||||
- "samode" # modify arbitrary channel and user modes
|
||||
- "snomasks" # subscribe to arbitrary server notice masks
|
||||
- "roleplay" # use the (deprecated) roleplay commands in any channel
|
||||
|
||||
# server admin: has full control of the ircd, including nickname and
|
||||
# channel registrations
|
||||
@ -605,11 +733,13 @@ oper-classes:
|
||||
|
||||
# capability names
|
||||
capabilities:
|
||||
- "rehash"
|
||||
- "accreg"
|
||||
- "chanreg"
|
||||
- "history"
|
||||
- "defcon"
|
||||
- "rehash" # rehash the server, i.e. reload the config at runtime
|
||||
- "accreg" # modify arbitrary account registrations
|
||||
- "chanreg" # modify arbitrary channel registrations
|
||||
- "history" # modify or delete history messages
|
||||
- "defcon" # use the DEFCON command (restrict server capabilities)
|
||||
- "massmessage" # message all users on the server
|
||||
- "metadata" # modify arbitrary metadata on channels and users
|
||||
|
||||
# ircd operators
|
||||
opers:
|
||||
@ -618,26 +748,25 @@ opers:
|
||||
# which capabilities this oper has access to
|
||||
class: "server-admin"
|
||||
|
||||
# custom whois line
|
||||
# traditionally, operator status is visible to unprivileged users in
|
||||
# WHO and WHOIS responses. this can be disabled with 'hidden'.
|
||||
hidden: true
|
||||
|
||||
# custom whois line (if `hidden` is enabled, visible only to other operators)
|
||||
whois-line: is the server administrator
|
||||
|
||||
# custom hostname
|
||||
vhost: "staff"
|
||||
|
||||
# normally, operator status is visible to unprivileged users in WHO and WHOIS
|
||||
# responses. this can be disabled with 'hidden'. ('hidden' also causes the
|
||||
# 'vhost' line above to be ignored.)
|
||||
hidden: false
|
||||
# custom hostname (ignored if `hidden` is enabled)
|
||||
#vhost: "staff"
|
||||
|
||||
# modes are modes to auto-set upon opering-up. uncomment this to automatically
|
||||
# enable snomasks ("server notification masks" that alert you to server events;
|
||||
# see `/quote help snomasks` while opered-up for more information):
|
||||
#modes: +is acjknoqtuxv
|
||||
#modes: +is acdjknoqtuxv
|
||||
|
||||
# operators can be authenticated either by password (with the /OPER command),
|
||||
# or by certificate fingerprint, or both. if a password hash is set, then a
|
||||
# password is required to oper up (e.g., /OPER dan mypassword). to generate
|
||||
# the hash, use `oragono genpasswd`.
|
||||
# the hash, use `ergo genpasswd`.
|
||||
password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
|
||||
|
||||
# if a SHA-256 certificate fingerprint is configured here, then it will be
|
||||
@ -675,7 +804,7 @@ logging:
|
||||
# be logged, even if you explicitly include it
|
||||
#
|
||||
# useful types include:
|
||||
# * everything (usually used with exclusing some types below)
|
||||
# * everything (usually used with excluding some types below)
|
||||
# server server startup, rehash, and shutdown events
|
||||
# accounts account registration and authentication
|
||||
# channels channel creation and operations
|
||||
@ -697,7 +826,7 @@ logging:
|
||||
|
||||
# debug options
|
||||
debug:
|
||||
# when enabled, oragono will attempt to recover from certain kinds of
|
||||
# when enabled, Ergo will attempt to recover from certain kinds of
|
||||
# client-triggered runtime errors that would normally crash the server.
|
||||
# this makes the server more resilient to DoS, but could result in incorrect
|
||||
# behavior. deployments that would prefer to "start from scratch", e.g., by
|
||||
@ -711,9 +840,15 @@ debug:
|
||||
# set to `null`, "", leave blank, or omit to disable
|
||||
# pprof-listener: "localhost:6060"
|
||||
|
||||
# lock file preventing multiple instances of Ergo from accidentally being
|
||||
# started at once. comment out or set to the empty string ("") to disable.
|
||||
# this path is relative to the working directory; if your datastore.path
|
||||
# is absolute, you should use an absolute path here as well.
|
||||
lock-file: "ircd.lock"
|
||||
|
||||
# datastore configuration
|
||||
datastore:
|
||||
# path to the datastore
|
||||
# path to the database file (used to store account and channel registrations):
|
||||
path: ircd.db
|
||||
|
||||
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
||||
@ -728,10 +863,13 @@ datastore:
|
||||
port: 3306
|
||||
# if socket-path is set, it will be used instead of host:port
|
||||
#socket-path: "/var/run/mysqld/mysqld.sock"
|
||||
user: "oragono"
|
||||
user: "ergo"
|
||||
password: "hunter2"
|
||||
history-database: "oragono_history"
|
||||
history-database: "ergo_history"
|
||||
timeout: 3s
|
||||
max-conns: 4
|
||||
# this may be necessary to prevent middleware from closing your connections:
|
||||
#conn-max-lifetime: 180s
|
||||
|
||||
# languages config
|
||||
languages:
|
||||
@ -753,6 +891,9 @@ limits:
|
||||
# identlen is the max ident length allowed
|
||||
identlen: 20
|
||||
|
||||
# realnamelen is the maximum realname length allowed
|
||||
realnamelen: 150
|
||||
|
||||
# channellen is the max channel length allowed
|
||||
channellen: 64
|
||||
|
||||
@ -772,7 +913,7 @@ limits:
|
||||
whowas-entries: 100
|
||||
|
||||
# maximum length of channel lists (beI modes)
|
||||
chan-list-modes: 60
|
||||
chan-list-modes: 100
|
||||
|
||||
# maximum number of messages to accept during registration (prevents
|
||||
# DoS / resource exhaustion attacks):
|
||||
@ -802,6 +943,15 @@ fakelag:
|
||||
# sending any commands:
|
||||
cooldown: 2s
|
||||
|
||||
# exempt a certain number of command invocations per session from fakelag;
|
||||
# this is to speed up "resynchronization" of client state during reattach
|
||||
command-budgets:
|
||||
"CHATHISTORY": 16
|
||||
"MARKREAD": 16
|
||||
"MONITOR": 1
|
||||
"WHO": 4
|
||||
"WEBPUSH": 1
|
||||
|
||||
# the roleplay commands are semi-standardized extensions to IRC that allow
|
||||
# sending and receiving messages from pseudo-nicknames. this can be used either
|
||||
# for actual roleplaying, or for bridging IRC with other protocols.
|
||||
@ -819,6 +969,12 @@ roleplay:
|
||||
# add the real nickname, in parentheses, to the end of every roleplay message?
|
||||
add-suffix: true
|
||||
|
||||
# allow customizing the NUH's sent for NPC and SCENE commands
|
||||
# NPC: the first %s is the NPC name, the second is the user's real nick
|
||||
#npc-nick-mask: "*%s*!%s@npc.fakeuser.invalid"
|
||||
# SCENE: the %s is the client's real nick
|
||||
#scene-nick-mask: "=Scene=!%s@npc.fakeuser.invalid"
|
||||
|
||||
# external services can integrate with the ircd using JSON Web Tokens (https://jwt.io).
|
||||
# in effect, the server can sign a token attesting that the client is present on
|
||||
# the server, is a member of a particular channel, etc.
|
||||
@ -866,7 +1022,7 @@ history:
|
||||
|
||||
# maximum number of CHATHISTORY messages that can be
|
||||
# requested at once (0 disables support for CHATHISTORY)
|
||||
chathistory-maxmessages: 100
|
||||
chathistory-maxmessages: 1000
|
||||
|
||||
# maximum number of messages that can be replayed at once during znc emulation
|
||||
# (znc.in/playback, or automatic replay on initial reattach to a persistent client):
|
||||
@ -890,12 +1046,14 @@ history:
|
||||
# if query-cutoff is set to 'registration-time', this allows retrieval
|
||||
# of messages that are up to 'grace-period' older than the above cutoff.
|
||||
# if you use 'registration-time', this is recommended to allow logged-out
|
||||
# users to do session resumption / query history after disconnections.
|
||||
# users to query history after disconnections.
|
||||
grace-period: 1h
|
||||
|
||||
# options to store history messages in a persistent database (currently only MySQL).
|
||||
# in order to enable any of this functionality, you must configure a MySQL server
|
||||
# in the `datastore.mysql` section.
|
||||
# in the `datastore.mysql` section. enabling persistence overrides the history
|
||||
# size limits above (`channel-length`, `client-length`, etc.); persistent
|
||||
# history has no limits other than those imposed by expire-time.
|
||||
persistent:
|
||||
enabled: false
|
||||
|
||||
@ -917,7 +1075,8 @@ history:
|
||||
|
||||
# options to control how messages are stored and deleted:
|
||||
retention:
|
||||
# allow users to delete their own messages from history?
|
||||
# allow users to delete their own messages from history,
|
||||
# and channel operators to delete messages in their channel?
|
||||
allow-individual-delete: false
|
||||
|
||||
# if persistent history is enabled, create additional index tables,
|
||||
@ -933,7 +1092,7 @@ history:
|
||||
# if `default` is false, store TAGMSG containing any of these tags:
|
||||
whitelist:
|
||||
- "+draft/react"
|
||||
- "react"
|
||||
- "+react"
|
||||
|
||||
# if `default` is true, don't store TAGMSG containing any of these tags:
|
||||
#blacklist:
|
||||
@ -941,5 +1100,58 @@ history:
|
||||
# - "typing"
|
||||
|
||||
# whether to allow customization of the config at runtime using environment variables,
|
||||
# e.g., ORAGONO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||
allow-environment-overrides: true
|
||||
|
||||
# metadata support for setting key/value data on channels and nicknames.
|
||||
metadata:
|
||||
# can clients store metadata?
|
||||
enabled: true
|
||||
# how many keys can a client subscribe to?
|
||||
max-subs: 100
|
||||
# how many keys can be stored per entity?
|
||||
max-keys: 100
|
||||
# rate limiting for client metadata updates, which are expensive to process
|
||||
client-throttle:
|
||||
enabled: true
|
||||
duration: 2m
|
||||
max-attempts: 10
|
||||
|
||||
# experimental support for mobile push notifications
|
||||
# see the manual for potential security, privacy, and performance implications.
|
||||
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
|
||||
# with no public IP listeners, only Tor/I2P listeners).
|
||||
webpush:
|
||||
# are push notifications enabled at all?
|
||||
enabled: false
|
||||
# request timeout for POST'ing the http notification
|
||||
timeout: 10s
|
||||
# delay sending the notification for this amount of time, then suppress it
|
||||
# if the client sent MARKREAD to indicate that it was read on another device
|
||||
delay: 0s
|
||||
# subscriber field for the VAPID JWT authorization:
|
||||
#subscriber: "https://your-website.com/"
|
||||
# maximum number of push subscriptions per user
|
||||
max-subscriptions: 4
|
||||
# expiration time for a push subscription; it must be renewed within this time
|
||||
# by the client reconnecting to IRC. we also detect whether the client is no longer
|
||||
# successfully receiving push messages.
|
||||
expiration: 14d
|
||||
|
||||
# HTTP API. we strongly recommend leaving this disabled unless you have a specific
|
||||
# need for it.
|
||||
api:
|
||||
# is the API enabled at all?
|
||||
enabled: false
|
||||
# listen address:
|
||||
listener: "127.0.0.1:8089"
|
||||
# serve over TLS (strongly recommended if the listener is public):
|
||||
#tls:
|
||||
#cert: fullchain.pem
|
||||
#key: privkey.pem
|
||||
# one or more static bearer tokens accepted for HTTP bearer authentication.
|
||||
# these must be strong, unique, high-entropy printable ASCII strings.
|
||||
# to generate a new token, use `ergo gentoken` or:
|
||||
# python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
bearer-tokens:
|
||||
- "example"
|
||||
|
||||
26
distrib/SMF/README
Normal file
26
distrib/SMF/README
Normal file
@ -0,0 +1,26 @@
|
||||
Created 22/11/2021 by georg@lysergic.dev.
|
||||
|
||||
This directory contains Service Management Facility service files for ergo.
|
||||
These files should be compatible with current OpenSolaris / Illumos based operating systems. Tested on OpenIndiana.
|
||||
|
||||
Prerequesites:
|
||||
- ergo binary located at /opt/ergo/ergo
|
||||
- ergo configuration located at /opt/ergo/ircd.yaml (hardcoded)
|
||||
- ergo languages located at /opt/ergo/languages (to be compatible with default.yaml - you may adjust this path or disable languages in your custom ircd.yaml)
|
||||
- ergo certificate and key located at /opt/ergo/fullchain.pem /opt/ergo/privkey.pem (to be compatible with default.yaml - you may adjust these paths in your custom ircd.yaml)
|
||||
- `ergo` role user and `ergo` role group owning all of the above
|
||||
|
||||
Installation:
|
||||
- cp ergo.xml /lib/svc/manifest/network/
|
||||
- cp ergo /lib/svc/method/
|
||||
- svcadm restart manifest-import
|
||||
|
||||
Usage:
|
||||
- svcadm enable ergo (Start)
|
||||
- tail /var/svc/log/network-ergo:default.log (Check ergo log and SMF output)
|
||||
- svcs ergo (Check status)
|
||||
- svcadm refresh ergo (Reload manifest and ergo configuration)
|
||||
- svcadm disable ergo (Stop)
|
||||
|
||||
Notes:
|
||||
- Does not support multiple instances - spawns instance :default
|
||||
26
distrib/SMF/ergo
Executable file
26
distrib/SMF/ergo
Executable file
@ -0,0 +1,26 @@
|
||||
#!/sbin/sh
|
||||
#
|
||||
# SMF method script for ergo - used by manifest file ergo.xml
|
||||
# Created 22/11/2021 by georg@lysergic.dev
|
||||
|
||||
. /lib/svc/share/smf_include.sh
|
||||
|
||||
case $1 in
|
||||
'start')
|
||||
exec /opt/ergo/ergo run --conf /opt/ergo/ircd.yaml
|
||||
;;
|
||||
|
||||
'refresh' )
|
||||
exec pkill -1 -U ergo -x ergo
|
||||
;;
|
||||
'stop' )
|
||||
exec pkill -U ergo -x ergo
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 { start | refresh | stop }"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit $?
|
||||
48
distrib/SMF/ergo.xml
Normal file
48
distrib/SMF/ergo.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version='1.0'?>
|
||||
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
|
||||
<service_bundle type='manifest' name='ergo'>
|
||||
<service name='network/ergo' type='service' version='0'>
|
||||
<create_default_instance enabled="true"/>
|
||||
<single_instance/>
|
||||
<dependency name='fs-local' grouping='require_all' restart_on='none' type='service'>
|
||||
<service_fmri value='svc:/system/filesystem/local'/>
|
||||
</dependency>
|
||||
<dependency name='fs-autofs' grouping='optional_all' restart_on='none' type='service'>
|
||||
<service_fmri value='svc:/system/filesystem/autofs'/>
|
||||
</dependency>
|
||||
<dependency name='net-loopback' grouping='require_all' restart_on='none' type='service'>
|
||||
<service_fmri value='svc:/network/loopback'/>
|
||||
</dependency>
|
||||
<dependency name='net-physical' grouping='require_all' restart_on='none' type='service'>
|
||||
<service_fmri value='svc:/network/physical'/>
|
||||
</dependency>
|
||||
<dependency name='config_data' grouping='require_all' restart_on='restart' type='path'>
|
||||
<service_fmri value='file://localhost/opt/ergo/ircd.yaml'/>
|
||||
</dependency>
|
||||
<method_context working_directory="/opt/ergo">
|
||||
<method_credential user='ergo' group='ergo' />
|
||||
</method_context>
|
||||
<exec_method name='start' type='method' exec='/lib/svc/method/ergo start' timeout_seconds='20'>
|
||||
<method_context security_flags='aslr'/>
|
||||
</exec_method>
|
||||
<exec_method name='stop' type='method' exec='/lib/svc/method/ergo stop' timeout_seconds='20'/>
|
||||
<exec_method name='refresh' type='method' exec='/lib/svc/method/ergo refresh' timeout_seconds='20'/>
|
||||
<property_group name='general' type='framework'>
|
||||
<propval name='action_authorization' type='astring' value='solaris.smf.manage.ergo'/>
|
||||
</property_group>
|
||||
<property_group name='startd' type='framework'>
|
||||
<propval name='ignore_error' type='astring' value='core,signal'/>
|
||||
<propval name='duration' type='astring' value='child'/>
|
||||
</property_group>
|
||||
<stability value='Unstable'/>
|
||||
<template>
|
||||
<common_name>
|
||||
<loctext xml:lang='C'>IRC server</loctext>
|
||||
</common_name>
|
||||
<documentation>
|
||||
<doc_link name='ergo-manual' uri='https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md'/>
|
||||
<doc_link name='ergo-userguide' uri='https://github.com/ergochat/ergo/blob/master/docs/USERGUIDE.md'/>
|
||||
</documentation>
|
||||
</template>
|
||||
</service>
|
||||
</service_bundle>
|
||||
37
distrib/anope/anope2json.py
Normal file → Executable file
37
distrib/anope/anope2json.py
Normal file → Executable file
@ -1,8 +1,9 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import re
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
@ -46,8 +47,17 @@ def to_unixnano(timestamp):
|
||||
def file_to_objects(infile):
|
||||
result = []
|
||||
obj = None
|
||||
for line in infile:
|
||||
pieces = line.rstrip('\r\n').split(' ', maxsplit=2)
|
||||
while True:
|
||||
line = infile.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.rstrip(b'\r\n')
|
||||
try:
|
||||
line = line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
line = line.decode('utf-8', 'replace')
|
||||
logging.warning("line contained invalid utf8 data " + line)
|
||||
pieces = line.split(' ', maxsplit=2)
|
||||
if len(pieces) == 0:
|
||||
logging.warning("skipping blank line in db")
|
||||
continue
|
||||
@ -58,6 +68,9 @@ def file_to_objects(infile):
|
||||
obj = AnopeObject(pieces[1], {})
|
||||
elif pieces[0] == 'DATA':
|
||||
obj.kv[pieces[1]] = pieces[2]
|
||||
elif pieces[0] == 'ID':
|
||||
# not sure what these do?
|
||||
continue
|
||||
else:
|
||||
raise ValueError("unknown command found in anope db", pieces[0])
|
||||
return result
|
||||
@ -71,6 +84,19 @@ ANOPE_MODENAME_TO_MODE = {
|
||||
'SECRET': 's',
|
||||
}
|
||||
|
||||
# verify that a certfp appears to be a hex-encoded SHA-256 fingerprint;
|
||||
# if it's anything else, silently ignore it
|
||||
def validate_certfps(certobj):
|
||||
certfps = []
|
||||
for fingerprint in certobj.split():
|
||||
try:
|
||||
dec = binascii.unhexlify(fingerprint)
|
||||
except:
|
||||
continue
|
||||
if len(dec) == 32:
|
||||
certfps.append(fingerprint)
|
||||
return certfps
|
||||
|
||||
def convert(infile):
|
||||
out = {
|
||||
'version': 1,
|
||||
@ -87,6 +113,9 @@ def convert(infile):
|
||||
if obj.type == 'NickCore':
|
||||
username = obj.kv['display']
|
||||
userdata = {'name': username, 'hash': obj.kv['pass'], 'email': obj.kv['email']}
|
||||
certobj = obj.kv.get('cert')
|
||||
if certobj:
|
||||
userdata['certfps'] = validate_certfps(certobj)
|
||||
out['users'][username] = userdata
|
||||
elif obj.type == 'NickAlias':
|
||||
username = obj.kv['nc']
|
||||
@ -167,7 +196,7 @@ def convert(infile):
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
raise Exception("Usage: anope2json.py anope.db output.json")
|
||||
with open(sys.argv[1]) as infile:
|
||||
with open(sys.argv[1], 'rb') as infile:
|
||||
output = convert(infile)
|
||||
with open(sys.argv[2], 'w') as outfile:
|
||||
json.dump(output, outfile)
|
||||
|
||||
34
distrib/apparmor/ergo
Normal file
34
distrib/apparmor/ergo
Normal file
@ -0,0 +1,34 @@
|
||||
include <tunables/global>
|
||||
|
||||
# Georg Pfuetzenreuter <georg+ergo@lysergic.dev>
|
||||
# AppArmor confinement for ergo and ergo-ldap
|
||||
|
||||
profile ergo /usr/bin/ergo {
|
||||
include <abstractions/base>
|
||||
include <abstractions/consoles>
|
||||
include <abstractions/nameservice>
|
||||
|
||||
/etc/ergo/ircd.{motd,yaml} r,
|
||||
/etc/ssl/irc/{crt,key} r,
|
||||
/etc/ssl/ergo/{crt,key} r,
|
||||
/usr/bin/ergo mr,
|
||||
/proc/sys/net/core/somaxconn r,
|
||||
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
|
||||
/usr/share/ergo/languages/{,*.lang.json,*.yaml} r,
|
||||
owner /run/ergo/ircd.lock rwk,
|
||||
owner /var/lib/ergo/ircd.db rw,
|
||||
|
||||
include if exists <local/ergo>
|
||||
|
||||
}
|
||||
|
||||
profile ergo-ldap /usr/bin/ergo-ldap {
|
||||
include <abstractions/openssl>
|
||||
include <abstractions/ssl_certs>
|
||||
|
||||
/usr/bin/ergo-ldap rm,
|
||||
/etc/ergo/ldap.yaml r,
|
||||
|
||||
include if exists <local/ergo-ldap>
|
||||
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@ -19,6 +20,14 @@ CMODE_FLAG_TO_MODE = {
|
||||
0x100: 't', # CMODE_TOPIC
|
||||
}
|
||||
|
||||
# attempt to interpret certfp as a hex-encoded SHA-256 fingerprint
|
||||
def validate_certfp(certfp):
|
||||
try:
|
||||
dec = binascii.unhexlify(certfp)
|
||||
except:
|
||||
return False
|
||||
return len(dec) == 32
|
||||
|
||||
def convert(infile):
|
||||
out = {
|
||||
'version': 1,
|
||||
@ -31,8 +40,16 @@ def convert(infile):
|
||||
|
||||
channel_to_founder = defaultdict(lambda: (None, None))
|
||||
|
||||
for line in infile:
|
||||
line = line.rstrip('\r\n')
|
||||
while True:
|
||||
line = infile.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.rstrip(b'\r\n')
|
||||
try:
|
||||
line = line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
line = line.decode('utf-8', 'replace')
|
||||
logging.warning("line contained invalid utf8 data " + line)
|
||||
parts = line.split(' ')
|
||||
category = parts[0]
|
||||
|
||||
@ -62,6 +79,11 @@ def convert(infile):
|
||||
if parts[2] == 'private:usercloak':
|
||||
username = parts[1]
|
||||
out['users'][username]['vhost'] = parts[3]
|
||||
elif category == 'MCFP':
|
||||
username, certfp = parts[1], parts[2]
|
||||
if validate_certfp(certfp):
|
||||
user = out['users'][username]
|
||||
user.setdefault('certfps', []).append(certfp.lower())
|
||||
elif category == 'MC':
|
||||
# channel registration
|
||||
# MC #mychannel 1600134478 1600467343 +v 272 0 0
|
||||
@ -177,7 +199,7 @@ def convert(infile):
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
raise Exception("Usage: atheme2json.py atheme_db output.json")
|
||||
with open(sys.argv[1]) as infile:
|
||||
with open(sys.argv[1], 'rb') as infile:
|
||||
output = convert(infile)
|
||||
with open(sys.argv[2], 'w') as outfile:
|
||||
json.dump(output, outfile)
|
||||
|
||||
29
distrib/bsd-rc/README.md
Normal file
29
distrib/bsd-rc/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
Ergo init script for bsd-rc
|
||||
===
|
||||
|
||||
Written for and tested using FreeBSD.
|
||||
|
||||
## Installation
|
||||
Copy the `ergo` file from this folder to `/etc/rc.d/ergo`,
|
||||
permissions should be `555`.
|
||||
|
||||
You should create a system user for Ergo.
|
||||
This script defaults to running Ergo as a user named `ergo`,
|
||||
but that can be changed using `/etc/rc.conf`.
|
||||
|
||||
Here are all `rc.conf` variables and their defaults:
|
||||
- `ergo_enable`, defaults to `NO`. Whether to run `ergo` at system start.
|
||||
- `ergo_user`, defaults to `ergo`. Run using this user.
|
||||
- `ergo_group`, defaults to `ergo`. Run using this group.
|
||||
- `ergo_chdir`, defaults to `/var/db/ergo`. Path to the working directory for the server. Should be writable for `ergo_user`.
|
||||
- `ergo_conf`, defaults to `/usr/local/etc/ergo/ircd.yaml`. Config file path. Make sure `ergo_user` can read it.
|
||||
|
||||
This script assumes ergo to be installed at `/usr/local/bin/ergo`.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
/etc/rc.d/ergo <command>
|
||||
```
|
||||
In addition to the obvious `start` and `stop` commands, this
|
||||
script also has a `reload` command that sends `SIGHUP` to the Ergo process.
|
||||
45
distrib/bsd-rc/ergo
Normal file
45
distrib/bsd-rc/ergo
Normal file
@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
|
||||
# PROVIDE: ergo
|
||||
# REQUIRE: DAEMON
|
||||
# KEYWORD: shutdown
|
||||
|
||||
#
|
||||
# Add the following lines to /etc/rc.conf to enable Ergo
|
||||
#
|
||||
# ergo_enable (bool): Set to YES to enable ergo.
|
||||
# Default is "NO".
|
||||
# ergo_user (user): Set user to run ergo.
|
||||
# Default is "ergo".
|
||||
# ergo_group (group): Set group to run ergo.
|
||||
# Default is "ergo".
|
||||
# ergo_config (file): Set ergo config file path.
|
||||
# Default is "/usr/local/etc/ergo/config.yaml".
|
||||
# ergo_chdir (dir): Set ergo working directory
|
||||
# Default is "/var/db/ergo".
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name=ergo
|
||||
rcvar=ergo_enable
|
||||
desc="Ergo IRCv3 server"
|
||||
|
||||
load_rc_config "$name"
|
||||
|
||||
: ${ergo_enable:=NO}
|
||||
: ${ergo_user:=ergo}
|
||||
: ${ergo_group:=ergo}
|
||||
: ${ergo_chdir:=/var/db/ergo}
|
||||
: ${ergo_conf:=/usr/local/etc/ergo/ircd.yaml}
|
||||
|
||||
# If you don't define a custom reload function,
|
||||
# rc automagically sends SIGHUP to the process on reload.
|
||||
# But you have to list reload as an extra_command for that.
|
||||
extra_commands="reload"
|
||||
|
||||
procname="/usr/local/bin/${name}"
|
||||
command=/usr/sbin/daemon
|
||||
command_args="-S -T ${name} ${procname} run --conf ${ergo_conf}"
|
||||
|
||||
run_rc_command "$1"
|
||||
|
||||
@ -1,33 +1,35 @@
|
||||
# Oragono Docker
|
||||
# Ergo Docker
|
||||
|
||||
This folder holds Oragono's Dockerfile and related materials. Oragono
|
||||
is published automatically to Docker Hub at
|
||||
[oragono/oragono](https://hub.docker.com/r/oragono/oragono).
|
||||
This folder holds Ergo's Docker compose file. The Dockerfile is in the root
|
||||
directory. Ergo is published automatically to the GitHub Container Registry at
|
||||
[ghcr.io/ergochat/ergo](https://ghcr.io/ergochat/ergo).
|
||||
|
||||
The `latest` tag tracks the `stable` branch of Oragono, which contains
|
||||
the latest stable release. The `dev` tag tracks the master branch, which
|
||||
may by unstable and is not recommended for production.
|
||||
Most users should use either the `stable` tag (corresponding to the
|
||||
`stable` branch in git, which tracks the latest stable release), or
|
||||
a tag corresponding to a tagged version (e.g. `v2.8.0`). The `master`
|
||||
tag corresponds to the `master` branch, which is not recommended for
|
||||
production use. The `latest` tag is not recommended.
|
||||
|
||||
## Quick start
|
||||
|
||||
The Oragono docker image is designed to work out of the box - it comes with a
|
||||
The Ergo docker image is designed to work out of the box - it comes with a
|
||||
usable default config and will automatically generate self-signed TLS
|
||||
certificates. To get a working ircd, all you need to do is run the image and
|
||||
expose the ports:
|
||||
|
||||
```shell
|
||||
docker run --name oragono -d -p 6667:6667 -p 6697:6697 oragono/oragono:tag
|
||||
docker run --init --name ergo -d -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
||||
```
|
||||
|
||||
This will start Oragono and listen on ports 6667 (plain text) and 6697 (TLS).
|
||||
The first time Oragono runs it will create a config file with a randomised
|
||||
This will start Ergo and listen on ports 6667 (plain text) and 6697 (TLS).
|
||||
The first time Ergo runs it will create a config file with a randomised
|
||||
oper password. This is output to stdout, and you can view it with the docker
|
||||
logs command:
|
||||
|
||||
```shell
|
||||
# Assuming your container is named `oragono`; use `docker container ls` to
|
||||
# Assuming your container is named `ergo`; use `docker container ls` to
|
||||
# find the name if you're not sure.
|
||||
docker logs oragono
|
||||
docker logs ergo
|
||||
```
|
||||
|
||||
You should see a line similar to:
|
||||
@ -36,69 +38,74 @@ You should see a line similar to:
|
||||
Oper username:password is admin:cnn2tm9TP3GeI4vLaEMS
|
||||
```
|
||||
|
||||
We recommend the use of `--init` (`init: true` in docker-compose) to solve an
|
||||
edge case involving unreaped zombie processes when Ergo's script API is used
|
||||
for authentication or IP validation. For more details, see
|
||||
[krallin/tini#8](https://github.com/krallin/tini/issues/8).
|
||||
|
||||
## Persisting data
|
||||
|
||||
Oragono has a persistent data store, used to keep account details, channel
|
||||
Ergo has a persistent data store, used to keep account details, channel
|
||||
registrations, and so on. To persist this data across restarts, you can mount
|
||||
a volume at /ircd.
|
||||
|
||||
For example, to create a new docker volume and then mount it:
|
||||
|
||||
```shell
|
||||
docker volume create oragono-data
|
||||
docker run -d -v oragono-data:/ircd -p 6667:6667 -p 6697:6697 oragono/oragono:tag
|
||||
docker volume create ergo-data
|
||||
docker run --init --name ergo -d -v ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
||||
```
|
||||
|
||||
Or to mount a folder from your host machine:
|
||||
|
||||
```shell
|
||||
mkdir oragono-data
|
||||
docker run -d -v $(PWD)/oragono-data:/ircd -p 6667:6667 -p 6697:6697 oragono/oragono:tag
|
||||
mkdir ergo-data
|
||||
docker run --init --name ergo -d -v $(pwd)/ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
||||
```
|
||||
|
||||
## Customising the config
|
||||
|
||||
Oragono's config file is stored at /ircd/ircd.yaml. If the file does not
|
||||
Ergo's config file is stored at /ircd/ircd.yaml. If the file does not
|
||||
exist, the default config will be written out. You can copy the config from
|
||||
the container, edit it, and then copy it back:
|
||||
|
||||
```shell
|
||||
# Assuming that your container is named `oragono`, as above.
|
||||
docker cp oragono:/ircd/ircd.yaml .
|
||||
# Assuming that your container is named `ergo`, as above.
|
||||
docker cp ergo:/ircd/ircd.yaml .
|
||||
vim ircd.yaml # edit the config to your liking
|
||||
docker cp ircd.yaml oragono:/ircd/ircd.yaml
|
||||
docker cp ircd.yaml ergo:/ircd/ircd.yaml
|
||||
```
|
||||
|
||||
You can use the `/rehash` command to make Oragono reload its config, or
|
||||
You can use the `/rehash` command to make Ergo reload its config, or
|
||||
send it the HUP signal:
|
||||
|
||||
```shell
|
||||
docker kill -HUP oragono
|
||||
docker kill -s SIGHUP ergo
|
||||
```
|
||||
|
||||
## Using custom TLS certificates
|
||||
|
||||
TLS certs will by default be read from /ircd/tls.crt, with a private key
|
||||
in /ircd/tls.key. You can customise this path in the ircd.yaml file if
|
||||
TLS certs will by default be read from /ircd/fullchain.pem, with a private key
|
||||
in /ircd/privkey.pem. You can customise this path in the ircd.yaml file if
|
||||
you wish to mount the certificates from another volume. For information
|
||||
on using Let's Encrypt certificates, see
|
||||
[this manual entry](https://github.com/oragono/oragono/blob/master/docs/MANUAL.md#how-do-i-use-lets-encrypt-certificates).
|
||||
[this manual entry](https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md#using-valid-tls-certificates).
|
||||
|
||||
## Using docker-compose
|
||||
|
||||
This folder contains a sample docker-compose file which can be used
|
||||
to start an Oragono instance with ports exposed and data persisted in
|
||||
to start an Ergo instance with ports exposed and data persisted in
|
||||
a docker volume. Simply download the file and then bring it up:
|
||||
|
||||
```shell
|
||||
curl -O https://raw.githubusercontent.com/oragono/oragono/master/distrib/docker/docker-compose.yml
|
||||
curl -O https://raw.githubusercontent.com/ergochat/ergo/master/distrib/docker/docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
If you wish to manually build the docker image, you need to do so from
|
||||
the root of the Oragono repository (not the `distrib/docker` directory):
|
||||
the root of the Ergo repository (not the `distrib/docker` directory):
|
||||
|
||||
```shell
|
||||
docker build .
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
version: "3.2"
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
oragono:
|
||||
image: oragono/oragono:latest
|
||||
ergo:
|
||||
init: true
|
||||
image: ghcr.io/ergochat/ergo:stable
|
||||
ports:
|
||||
- "6667:6667/tcp"
|
||||
- "6697:6697/tcp"
|
||||
|
||||
9
distrib/docker/run.sh
Normal file → Executable file
9
distrib/docker/run.sh
Normal file → Executable file
@ -1,8 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
# start in right dir
|
||||
cd /ircd
|
||||
|
||||
# make config file
|
||||
if [ ! -f "/ircd/ircd.yaml" ]; then
|
||||
awk '{gsub(/path: languages/,"path: /ircd-bin/languages")}1' /ircd-bin/default.yaml > /tmp/ircd.yaml
|
||||
@ -10,7 +7,7 @@ if [ ! -f "/ircd/ircd.yaml" ]; then
|
||||
# change default oper passwd
|
||||
OPERPASS=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c20)
|
||||
echo "Oper username:password is admin:$OPERPASS"
|
||||
ENCRYPTEDPASS=$(echo "$OPERPASS" | /ircd-bin/oragono genpasswd)
|
||||
ENCRYPTEDPASS=$(echo "$OPERPASS" | /ircd-bin/ergo genpasswd)
|
||||
ORIGINALPASS='\$2a\$04\$0123456789abcdef0123456789abcdef0123456789abcdef01234'
|
||||
|
||||
awk "{gsub(/password: \\\"$ORIGINALPASS\\\"/,\"password: \\\"$ENCRYPTEDPASS\\\"\")}1" /tmp/ircd.yaml > /tmp/ircd2.yaml
|
||||
@ -23,7 +20,7 @@ if [ ! -f "/ircd/ircd.yaml" ]; then
|
||||
fi
|
||||
|
||||
# make self-signed certs if they don't already exist
|
||||
/ircd-bin/oragono mkcerts
|
||||
/ircd-bin/ergo mkcerts
|
||||
|
||||
# run!
|
||||
exec /ircd-bin/oragono run
|
||||
exec /ircd-bin/ergo run
|
||||
|
||||
57
distrib/init/rc.ergo
Normal file
57
distrib/init/rc.ergo
Normal file
@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Init script for the ergo IRCd
|
||||
# Created 14/06/2021 by georg@lysergic.dev
|
||||
# Desgigned for and tested on Slackware -current
|
||||
# Depends on `daemon` (installable using slackpkg)
|
||||
# In its stock configuration ergo will be jailed to /opt/ergo - all paths are relative from there. Consider this in your ergo configuration file (i.e. certificate, database and log locations)
|
||||
|
||||
NAME=ergo
|
||||
DIR=/opt/ergo
|
||||
ERGO=/ergo
|
||||
DAEMONIZER=/usr/bin/daemon
|
||||
CONFIG=ircd.yaml
|
||||
USER=ergo
|
||||
GROUP=ergo
|
||||
|
||||
daemon_start() {
|
||||
$DAEMONIZER -n $NAME -v -- chroot --userspec=$USER --groups=$USER -- $DIR $ERGO run --conf $CONFIG
|
||||
}
|
||||
|
||||
daemon_stop() {
|
||||
$DAEMONIZER --stop -n $NAME -v
|
||||
}
|
||||
|
||||
daemon_restart() {
|
||||
$DAEMONIZER --restart -n $NAME -v
|
||||
}
|
||||
|
||||
daemon_reload() {
|
||||
$DAEMONIZER --signal=SIGHUP -n $NAME -v
|
||||
}
|
||||
|
||||
daemon_status() {
|
||||
$DAEMONIZER --running -n $NAME -v
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
daemon_start
|
||||
;;
|
||||
stop)
|
||||
daemon_stop
|
||||
;;
|
||||
restart)
|
||||
daemon_restart
|
||||
;;
|
||||
reload)
|
||||
daemon_reload
|
||||
;;
|
||||
status)
|
||||
daemon_status
|
||||
;;
|
||||
*)
|
||||
echo "Source: https://github.com/ergochat/ergo"
|
||||
echo "Usage: $0 {start|stop|restart|reload|status}"
|
||||
exit 1
|
||||
esac
|
||||
3
distrib/openrc/ergo.confd
Normal file
3
distrib/openrc/ergo.confd
Normal file
@ -0,0 +1,3 @@
|
||||
# /etc/conf.d/ergo: config file for /etc/init.d/ergo
|
||||
ERGO_CONFIGFILE="/etc/ergo/ircd.yaml"
|
||||
ERGO_USERNAME="ergo"
|
||||
32
distrib/openrc/ergo.initd
Normal file
32
distrib/openrc/ergo.initd
Normal file
@ -0,0 +1,32 @@
|
||||
#!/sbin/openrc-run
|
||||
name=${RC_SVCNAME}
|
||||
description="ergo IRC daemon"
|
||||
|
||||
command=/usr/bin/ergo
|
||||
command_args="run --conf ${ERGO_CONFIGFILE:-'/etc/ergo/ircd.yaml'}"
|
||||
command_user=${ERGO_USERNAME:-ergo}
|
||||
command_background=true
|
||||
|
||||
pidfile=/var/run/${RC_SVCNAME}.pid
|
||||
|
||||
output_log="/var/log/${RC_SVCNAME}.out"
|
||||
error_log="/var/log/${RC_SVCNAME}.err"
|
||||
# --wait: to wait 1 second after launching to see if it survived startup
|
||||
start_stop_daemon_args="--wait 1000"
|
||||
|
||||
extra_started_commands="reload"
|
||||
|
||||
depend() {
|
||||
use dns
|
||||
provide ircd
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath --owner ${command_user}:${command_user} --mode 0640 --file /var/log/${RC_SVCNAME}.out /var/log/${RC_SVCNAME}.err
|
||||
}
|
||||
|
||||
reload() {
|
||||
ebegin "Reloading ${RC_SVCNAME}"
|
||||
start-stop-daemon --signal HUP --pidfile "${pidfile}"
|
||||
eend $?
|
||||
}
|
||||
8
distrib/s6/README
Normal file
8
distrib/s6/README
Normal file
@ -0,0 +1,8 @@
|
||||
This directory contains s6 srv and log services for ergo.
|
||||
|
||||
These services expect that ergo is installed to /opt/ergo,
|
||||
and an ergo system user that owns /opt/ergo.
|
||||
|
||||
To install:
|
||||
cp -r ergo-srv ergo-log /etc/s6/sv/
|
||||
cp ergo.conf /etc/s6/config/
|
||||
1
distrib/s6/ergo-log/consumer-for
Normal file
1
distrib/s6/ergo-log/consumer-for
Normal file
@ -0,0 +1 @@
|
||||
ergo-srv
|
||||
1
distrib/s6/ergo-log/notification-fd
Normal file
1
distrib/s6/ergo-log/notification-fd
Normal file
@ -0,0 +1 @@
|
||||
3
|
||||
1
distrib/s6/ergo-log/pipeline-name
Normal file
1
distrib/s6/ergo-log/pipeline-name
Normal file
@ -0,0 +1 @@
|
||||
ergo
|
||||
9
distrib/s6/ergo-log/run
Normal file
9
distrib/s6/ergo-log/run
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/execlineb -P
|
||||
envfile /etc/s6/config/ergo.conf
|
||||
importas -sCiu DIRECTIVES DIRECTIVES
|
||||
ifelse { test -w /var/log } {
|
||||
foreground { install -d -o s6log -g s6log /var/log/ergo }
|
||||
s6-setuidgid s6log exec -c s6-log -d3 -b -- ${DIRECTIVES} /var/log/ergo
|
||||
}
|
||||
foreground { install -d -o s6log -g s6log /run/log/ergo }
|
||||
s6-setuidgid s6log exec -c s6-log -d3 -b -- ${DIRECTIVES} /run/log/ergo
|
||||
1
distrib/s6/ergo-log/type
Normal file
1
distrib/s6/ergo-log/type
Normal file
@ -0,0 +1 @@
|
||||
longrun
|
||||
1
distrib/s6/ergo-srv/producer-for
Normal file
1
distrib/s6/ergo-srv/producer-for
Normal file
@ -0,0 +1 @@
|
||||
ergo-log
|
||||
4
distrib/s6/ergo-srv/run
Normal file
4
distrib/s6/ergo-srv/run
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/execlineb -P
|
||||
fdmove -c 2 1
|
||||
execline-cd /opt/ergo
|
||||
s6-setuidgid ergo ./ergo run
|
||||
1
distrib/s6/ergo-srv/type
Normal file
1
distrib/s6/ergo-srv/type
Normal file
@ -0,0 +1 @@
|
||||
longrun
|
||||
2
distrib/s6/ergo.conf
Normal file
2
distrib/s6/ergo.conf
Normal file
@ -0,0 +1,2 @@
|
||||
# This configures the directives used for s6-log in the log service.
|
||||
DIRECTIVES="n3 s2000000"
|
||||
@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=oragono
|
||||
Description=ergo
|
||||
After=network.target
|
||||
# If you are using MySQL for history storage, comment out the above line
|
||||
# and uncomment these two instead (you must independently install and configure
|
||||
@ -8,13 +8,14 @@ After=network.target
|
||||
# After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=oragono
|
||||
WorkingDirectory=/home/oragono
|
||||
ExecStart=/home/oragono/oragono run --conf /home/oragono/ircd.yaml
|
||||
Type=notify
|
||||
User=ergo
|
||||
WorkingDirectory=/home/ergo
|
||||
ExecStart=/home/ergo/ergo run --conf /home/ergo/ircd.yaml
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
LimitNOFILE=1048576
|
||||
NotifyAccess=main
|
||||
# Uncomment this for a hidden service:
|
||||
# PrivateNetwork=true
|
||||
|
||||
124
docs/API.md
Normal file
124
docs/API.md
Normal file
@ -0,0 +1,124 @@
|
||||
__ __ ______ ___ ______ ___
|
||||
__/ // /_/ ____/ __ \/ ____/ __ \
|
||||
/_ // __/ __/ / /_/ / / __/ / / /
|
||||
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
||||
/_//_/ /_____/_/ |_|\____/\____/
|
||||
|
||||
Ergo IRCd API Documentation
|
||||
https://ergo.chat/
|
||||
|
||||
_Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn@cs.stanford.edu>_
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------------------
|
||||
|
||||
Ergo has an experimental HTTP API. Some general information about the API:
|
||||
|
||||
1. All requests to the API are via POST.
|
||||
1. All requests to the API are authenticated via bearer authentication. This is a header named `Authorization` with the value `Bearer <token>`. A list of valid tokens is hardcoded in the Ergo config. Future versions of Ergo may allow additional validation schemes for tokens.
|
||||
1. The request parameters are sent as JSON in the POST body.
|
||||
1. Any status code other than 200 is an error response; the response body is undefined in this case (likely human-readable text for debugging).
|
||||
1. A 200 status code indicates successful execution of the request. The response body will be JSON and may indicate application-level success or failure (typically via the `success` field, which takes a boolean value).
|
||||
|
||||
API endpoints are versioned (currently all endpoints have a `/v1/` path prefix). Backwards-incompatible updates will most likely take the form of endpoints with new names, or an increased version prefix. Any exceptions to this will be specifically documented in the changelog.
|
||||
|
||||
All API endpoints should be considered highly privileged. Bearer tokens should be kept secret. Access to the API should be either over a trusted link (like loopback) or secured via verified TLS. See the `api` section of `default.yaml` for examples of how to configure this.
|
||||
|
||||
Here's an example of how to test an API configured to run over loopback TCP in plaintext:
|
||||
|
||||
```bash
|
||||
curl -d '{"accountName": "invalidaccountname", "passphrase": "invalidpassphrase"}' -H 'Authorization: Bearer EYBbXVilnumTtfn4A9HE8_TiKLGWEGylre7FG6gEww0' -v http://127.0.0.1:8089/v1/check_auth
|
||||
```
|
||||
|
||||
This returns:
|
||||
|
||||
```json
|
||||
{"success":false}
|
||||
```
|
||||
|
||||
Endpoints
|
||||
=========
|
||||
|
||||
`/v1/account_details`
|
||||
----------------
|
||||
|
||||
This endpoint fetches account details and returns them as JSON. The request is a JSON object with fields:
|
||||
|
||||
* `accountName`: string, name of the account
|
||||
|
||||
The response is a JSON object with fields:
|
||||
|
||||
* `success`: whether the account exists or not
|
||||
* `accountName`: canonical, case-unfolded version of the account name
|
||||
* `email`: email address of the account provided
|
||||
* `registeredAt`: string, registration date/time of the account (in ISO8601 format)
|
||||
* `channels`: array of strings, list of channels the account is registered on or associated with
|
||||
|
||||
`/v1/check_auth`
|
||||
----------------
|
||||
|
||||
This endpoint verifies the credentials of a NickServ account; this allows Ergo to be used as the source of truth for authentication by another system. The request is a JSON object with fields:
|
||||
|
||||
* `accountName`: string, name of the account
|
||||
* `passphrase`: string, alleged passphrase of the account
|
||||
|
||||
The response is a JSON object with fields:
|
||||
|
||||
* `success`: whether the credentials provided were valid
|
||||
* `accountName`: canonical, case-unfolded version of the account name
|
||||
|
||||
`/v1/rehash`
|
||||
------------
|
||||
|
||||
This endpoint rehashes the server (i.e. reloads the configuration file, TLS certificates, and other associated data). The body is ignored. The response is a JSON object with fields:
|
||||
|
||||
* `success`: boolean, indicates whether the rehash was successful
|
||||
* `error`: string, optional, human-readable description of the failure
|
||||
|
||||
`/v1/saregister`
|
||||
----------------
|
||||
|
||||
This endpoint registers an account in NickServ, with the same semantics as `NS SAREGISTER`. The request is a JSON object with fields:
|
||||
|
||||
* `accountName`: string, name of the account
|
||||
* `passphrase`: string, passphrase of the account
|
||||
|
||||
The response is a JSON object with fields:
|
||||
|
||||
* `success`: whether the account creation succeeded
|
||||
* `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`.
|
||||
* `error`: string, optional, human-readable description of the failure.
|
||||
|
||||
`/v1/account_list`
|
||||
-------------------
|
||||
|
||||
This endpoint fetches a list of all accounts. The request body is ignored and can be empty.
|
||||
|
||||
The response is a JSON object with fields:
|
||||
|
||||
* `success`: whether the request succeeded
|
||||
* `accounts`: array of objects, each with fields:
|
||||
* `success`: boolean, whether this individual account query succeeded
|
||||
* `accountName`: string, canonical, case-unfolded version of the account name
|
||||
* `totalCount`: integer, total number of accounts returned
|
||||
|
||||
|
||||
`/v1/status`
|
||||
-------------
|
||||
|
||||
This endpoint returns status information about the running Ergo server. The request body is ignored and can be empty.
|
||||
|
||||
The response is a JSON object with fields:
|
||||
|
||||
* `success`: whether the request succeeded
|
||||
* `version`: string, Ergo server version string
|
||||
* `go_version`: string, version of Go runtime used
|
||||
* `start_time`: string, server start time in ISO8601 format
|
||||
* `users`: object with fields:
|
||||
* `total`: total number of users connected
|
||||
* `invisible`: number of invisible users
|
||||
* `operators`: number of operators connected
|
||||
* `unknown`: number of users with unknown status
|
||||
* `max`: maximum number of users seen connected at once
|
||||
* `channels`: integer, number of channels currently active
|
||||
* `servers`: integer, number of servers connected in the network
|
||||
501
docs/MANUAL.md
501
docs/MANUAL.md
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,11 @@
|
||||
__ __ ______ ___ ______ ___
|
||||
__/ // /_/ ____/ __ \/ ____/ __ \
|
||||
/_ // __/ __/ / /_/ / / __/ / / /
|
||||
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
||||
/_//_/ /_____/_/ |_|\____/\____/
|
||||
|
||||
▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
|
||||
▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
|
||||
▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄
|
||||
▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌
|
||||
▀█▄▀▪.▀ ▀ ▀ ▀ ·▀▀▀▀ ▀█▄▀ ▀▀ █▪ ▀█▄▀▪
|
||||
|
||||
Oragono IRCd User Guide
|
||||
https://oragono.io/
|
||||
Ergo IRCd User Guide
|
||||
https://ergo.chat/
|
||||
|
||||
_Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn@cs.stanford.edu>_
|
||||
|
||||
@ -18,22 +17,24 @@ _Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [About IRC](#about-irc)
|
||||
- [How Oragono is different](#how-oragono-is-different)
|
||||
- [How Ergo is different](#how-ergo-is-different)
|
||||
- [Account registration](#account-registration)
|
||||
- [Channel registration](#channel-registration)
|
||||
- [Always-on](#always-on)
|
||||
- [Multiclient](#multiclient)
|
||||
- [History](#history)
|
||||
- [Push notifications](#push-notifications)
|
||||
|
||||
--------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Introduction
|
||||
|
||||
Welcome to Oragono, a modern IRC server!
|
||||
Welcome to Ergo, a modern IRC server!
|
||||
|
||||
This guide is for end users of Oragono (people using Oragono to chat). If you're installing your own Oragono instance, you should consult the official manual instead (a copy should be bundled with your release, in the `docs/` directory).
|
||||
This guide is for end users of Ergo (people using Ergo to chat). If you're installing your own Ergo instance, you should consult the official manual instead (a copy should be bundled with your release, in the `docs/` directory).
|
||||
|
||||
This guide assumes that Oragono is in its default or recommended configuration; Oragono server administrators can change settings to make the server behave differently. If something isn't working as expected, ask your server administrator for help.
|
||||
This guide assumes that Ergo is in its default or recommended configuration; Ergo server administrators can change settings to make the server behave differently. If something isn't working as expected, ask your server administrator for help.
|
||||
|
||||
# About IRC
|
||||
|
||||
@ -46,24 +47,24 @@ Here are some guides covering the basics of IRC:
|
||||
* [Fedora Magazine: Beginner's Guide to IRC](https://fedoramagazine.org/beginners-guide-irc/)
|
||||
* [IRCHelp's IRC Tutorial](https://www.irchelp.org/faq/irctutorial.html) (in particular, section 3, "Beyond the Basics")
|
||||
|
||||
# How Oragono is different
|
||||
# How Ergo is different
|
||||
|
||||
Oragono differs in many ways from conventional IRC servers. If you're *not* familiar with other IRC servers, you may want to skip this section. Here are some of the most salient differences:
|
||||
Ergo differs in many ways from conventional IRC servers. If you're *not* familiar with other IRC servers, you may want to skip this section. Here are some of the most salient differences:
|
||||
|
||||
* Oragono integrates a "bouncer" into the server. In particular:
|
||||
* Oragono stores message history for later retrieval.
|
||||
* Ergo integrates a "bouncer" into the server. In particular:
|
||||
* Ergo stores message history for later retrieval.
|
||||
* You can be "present" on the server (joined to channels, able to receive DMs) without having an active client connection to the server.
|
||||
* Conversely, you can use multiple clients to view / control the same presence (nickname) on the server, as long as you authenticate with SASL when connecting.
|
||||
* Oragono integrates "services" into the server. In particular:
|
||||
* Nicknames are strictly reserved: once you've registered your nickname, you must log in in order to use it. Consequently, SASL is more important when using Oragono than in other systems.
|
||||
* Ergo integrates "services" into the server. In particular:
|
||||
* Nicknames are strictly reserved: once you've registered your nickname, you must log in in order to use it. Consequently, SASL is more important when using Ergo than in other systems.
|
||||
* All properties of registered channels are protected without the need for `ChanServ` to be joined to the channel.
|
||||
* Oragono "cloaks", i.e., cryptographically scrambles, end user IPs so that they are not displayed publicly.
|
||||
* By default, the user/ident field is inoperative in Oragono: it is always set to `~u`, regardless of the `USER` command or the client's support for identd. This is because it is not in general a reliable or trustworthy way to distinguish users coming from the same IP. Oragono's integrated bouncer features should reduce the need for shared shell hosts and hosted bouncers (one of the main remaining use cases for identd).
|
||||
* By default, Oragono is only accessible via TLS.
|
||||
* Ergo "cloaks", i.e., cryptographically scrambles, end user IPs so that they are not displayed publicly.
|
||||
* By default, the user/ident field is inoperative in Ergo: it is always set to `~u`, regardless of the `USER` command or the client's support for identd. This is because it is not in general a reliable or trustworthy way to distinguish users coming from the same IP. Ergo's integrated bouncer features should reduce the need for shared shell hosts and hosted bouncers (one of the main remaining use cases for identd).
|
||||
* By default, Ergo is only accessible via TLS.
|
||||
|
||||
# Account registration
|
||||
|
||||
Although (as in other IRC systems) basic chat functionality is available without creating an account, most of Oragono's features require an account. You can create an account by sending a direct message to `NickServ`. (In IRC jargon, `NickServ` is a "network service", but if you're not familiar with the concept you can just think of it as a bot or a text user interface.) In a typical client, this will be:
|
||||
Although (as in other IRC systems) basic chat functionality is available without creating an account, most of Ergo's features require an account. You can create an account by sending a direct message to `NickServ`. (In IRC jargon, `NickServ` is a "network service", but if you're not familiar with the concept you can just think of it as a bot or a text user interface.) In a typical client, this will be:
|
||||
|
||||
```
|
||||
/msg NickServ register mySecretPassword validEmailAddress@example.com
|
||||
@ -71,10 +72,12 @@ Although (as in other IRC systems) basic chat functionality is available without
|
||||
|
||||
This registers your current nickname as your account name, with the password `mySecretPassword` (replace this with your own secret password!)
|
||||
|
||||
Once you have registered your account, you must configure SASL in your client, so that you will be logged in automatically on each connection. [Freenode's SASL guide](https://freenode.net/kb/answer/sasl) covers most popular clients.
|
||||
Once you have registered your account, you must configure SASL in your client, so that you will be logged in automatically on each connection. [libera.chat's SASL guide](https://libera.chat/guides/sasl) covers most popular clients.
|
||||
|
||||
If your client doesn't support SASL, you can typically use the "server password" (`PASS`) field in your client to log into your account automatically when connecting. Set the server password to `accountname:accountpassword`, where `accountname` is your account name and `accountpassword` is your account password.
|
||||
|
||||
For information on how to use a client certificate for authentication, see the [operator manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md#client-certificates).
|
||||
|
||||
# Channel registration
|
||||
|
||||
Once you've registered your nickname, you can use it to register channels. By default, channels are ephemeral; they go away when there are no longer any users in the channel, or when the server is restarted. Registering a channel gives you permanent control over it, and ensures that its settings will persist. To register a channel, send a message to `ChanServ`:
|
||||
@ -83,23 +86,27 @@ Once you've registered your nickname, you can use it to register channels. By de
|
||||
/msg ChanServ register #myChannel
|
||||
```
|
||||
|
||||
You must already be an operator (have the `+o` channel mode --- your client may display this as an `@` next to your nickname). If you're not a channel operator in the channel you want to register, ask your server administrator for help.
|
||||
The channel must exist (if it doesn't, you can create it with `/join #myChannel`) and you must already be an operator (have the `+o` channel mode --- your client may display this as an `@` next to your nickname). If you're not a channel operator in the channel you want to register, ask your server administrator for help.
|
||||
|
||||
# Always-on
|
||||
|
||||
By default, if you lose your connection to the IRC server, you are no longer present on the server; other users will see that you have "quit", you will no longer appear in channel lists, and you will not be able to receive direct messages. Oragono supports "always-on clients", where you remain on the server even when you are disconnected. To enable this, you can send a message to `NickServ`:
|
||||
By default, if you lose your connection to the IRC server, you are no longer present on the server; other users will see that you have "quit", you will no longer appear in channel lists, and you will not be able to receive direct messages. Ergo supports "always-on clients", where you remain on the server even when you are disconnected. To enable this, you can send a message to `NickServ`:
|
||||
|
||||
```
|
||||
/msg NickServ set always-on true
|
||||
```
|
||||
|
||||
# Multiclient
|
||||
|
||||
Ergo natively supports attaching multiple clients to the same nickname (this normally requires the use of an external bouncer, like ZNC or WeeChat's "relay" functionality). To use this feature, simply authenticate with SASL (or the PASS workaround, if necessary) when connecting. In the recommended configuration of Ergo, you will receive the nickname associated with your account, even if you have other clients already using it.
|
||||
|
||||
# History
|
||||
|
||||
Oragono stores message history on the server side (typically not an unlimited amount --- consult your server's FAQ, or your server administrator, to find out how much is being stored and how long it's being retained).
|
||||
Ergo stores message history on the server side (typically not an unlimited amount --- consult your server's FAQ, or your server administrator, to find out how much is being stored and how long it's being retained).
|
||||
|
||||
1. The [IRCv3 chathistory specification](https://ircv3.net/specs/extensions/chathistory) offers the most fine-grained control over history replay. It is supported by [Kiwi IRC](https://github.com/kiwiirc/kiwiirc), and hopefully other clients soon.
|
||||
1. The [IRCv3 chathistory specification](https://ircv3.net/specs/extensions/chathistory) offers the most fine-grained control over history replay. It is supported by [Gamja](https://git.sr.ht/~emersion/gamja), [Goguma](https://sr.ht/~emersion/goguma/), and [Kiwi IRC](https://github.com/kiwiirc/kiwiirc), and hopefully other clients soon.
|
||||
1. We emulate the [ZNC playback module](https://wiki.znc.in/Playback) for clients that support it. You may need to enable support for it explicitly in your client. For example, in [Textual](https://www.codeux.com/textual/), go to "Server properties", select "Vendor specific", uncheck "Do not automatically join channels on connect", and check "Only play back messages you missed". ZNC's wiki page covers other common clients (although if the feature is only supported via a script or third-party extension, the following option may be easier).
|
||||
1. If you set your client to always-on (see the previous section for details), you can set a "device ID" for each device you use. Oragono will then remember the last time your device was present on the server, and each time you sign on, it will attempt to replay exactly those messages you missed. There are a few ways to set your device ID when connecting:
|
||||
1. If you set your client to always-on (see the previous section for details), you can set a "device ID" for each device you use. Ergo will then remember the last time your device was present on the server, and each time you sign on, it will attempt to replay exactly those messages you missed. There are a few ways to set your device ID when connecting:
|
||||
- You can add it to your SASL username with an `@`, e.g., if your SASL username is `alice` you can send `alice@phone`
|
||||
- You can add it in a similar way to your IRC protocol username ("ident"), e.g., `alice@phone`
|
||||
- If login to user accounts via the `PASS` command is enabled on the server, you can provide it there, e.g., by sending `alice@phone:hunter2` as the server password
|
||||
@ -113,5 +120,9 @@ If you have registered a channel, you can make it private. The best way to do th
|
||||
|
||||
1. Set your channel to be invite-only (`/mode #example +i`)
|
||||
1. Identify the users you want to be able to access the channel. Ensure that they have registered their accounts (you should be able to see their registration status if you `/WHOIS` their nicknames).
|
||||
1. Add the desired nick/account names to the invite exception list (`/mode #example +I alice`)
|
||||
1. Add the desired nick/account names to the invite exception list (`/mode #example +I alice`) or give them persistent voice (`/msg ChanServ AMODE #example +v alice`)
|
||||
1. If you want to grant a persistent channel privilege to a user, you can do it with `CS AMODE` (`/msg ChanServ AMODE #example +o bob`)
|
||||
|
||||
# Push notifications
|
||||
|
||||
Ergo has experimental support for mobile push notifications. The server operator must enable this functionality; to check whether this is the case, you can send `/msg NickServ push list`. You must additionally be using a client (e.g. Goguma) that supports the functionality, and your account must be set to always-on (`/msg NickServ set always-on true`, as described above).
|
||||
|
||||
BIN
docs/logo.png
BIN
docs/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 9.1 KiB |
@ -1 +1,2 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 209"><defs><style>.cls-1{fill:#0f0f0f;}.cls-2{fill:#6a83c1;}.cls-3{fill:#9eb6de;}</style></defs><title>logo</title><path class="cls-1" d="M96.63,94v21.95H73.8V137H62.39v39.89H73.8v21.95H96.63V220H51V198.87H28.13V178.52H16.72V135.44H28.13V115.91H51V94H96.63Z" transform="translate(-12 -11)"/><path class="cls-1" d="M621.85,94v21.95H599V137H587.6v39.89H599v21.95h22.84V220H576.18V198.87H553.34V178.52H541.93V135.44h11.41V115.91h22.84V94h45.67Z" transform="translate(-12 -11)"/><path class="cls-1" d="M713.19,11V52.48h11.43V94H736v41.48h11.43V54.07H736V32.95h22.84V52.48h22.84V95.56H770.29v83H758.86V220H736V178.52H724.62v-83H713.19V198.87H667.52V135.44h11.43V94h11.41V52.48h11.43V11h11.41Z" transform="translate(-12 -11)"/><path class="cls-1" d="M873,94v21.95H850.2V137H838.79v39.89H850.2v21.95H873V220H827.37V198.87H804.53V178.52H793.12V135.44h11.41V115.91h22.84V94H873Z" transform="translate(-12 -11)"/><path class="cls-2" d="M154.07,204.11a2.45,2.45,0,0,1,1.78.73,2.51,2.51,0,0,1,0,3.57,2.47,2.47,0,0,1-1.77.72H153.4a2.45,2.45,0,0,1-1.78-.73,2.51,2.51,0,0,1,0-3.57,2.48,2.48,0,0,1,1.78-.72h0.67Z" transform="translate(-12 -11)"/><path class="cls-3" d="M775,192.06v9.41h-9.42v-9.41H775Z" transform="translate(-12 -11)"/><path class="cls-3" d="M135.6,192.06v9.41h-9.42v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M912,192.06v9.41h-9.42v-9.41H912Z" transform="translate(-12 -11)"/><path class="cls-1" d="M895.87,115.91v19.54H907.3v43.07H895.87v20.35H873V176.93h11.43V137H873V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M644.68,115.91v19.54h11.43v43.07H644.68v20.35H621.85V176.93h11.43V137H621.85V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M462,32.95V52.48h22.84V74.43H462V54.07H439.16V157.39H462v19.54h34.26V137H484.84V115.91H462V94h45.67v21.95h22.84v62.61H507.67v20.35H416.33V178.52H404.92V137H393.49V115.91h22.84V95.56H404.92V52.48h11.41V32.95H462Z" transform="translate(-12 -11)"/><path class="cls-1" d="M210.81,32.95V52.48h22.84V95.56H210.81v20.35H165.14v19.54H188v63.43H165.14V178.52H153.73V94h11.41V74.43H188V94h22.84V54.07H165.14V74.43H142.31V32.95h68.51Z" transform="translate(-12 -11)"/><path class="cls-1" d="M119.47,115.91v19.54H130.9v43.07H119.47v20.35H96.63V176.93h11.43V137H96.63V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M233.65,115.91v19.54h11.43v41.48h11.41v21.95H233.65V178.52H210.81V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M347.82,32.95V52.48h22.84v83h11.43v43.07H370.66v20.35H347.82V176.93h11.43V137H347.82V115.91H302.15v83H279.32V178.52H267.91V137H256.48V115.91h22.84V95.56H267.91V52.48h11.41V32.95h68.51ZM325,54.07H302.15V94h45.67V74.43H325V54.07Z" transform="translate(-12 -11)"/><path class="cls-2" d="M408.56,194a1.9,1.9,0,0,1,0,3.81,1.85,1.85,0,0,1-1.36-.56,1.9,1.9,0,0,1,0-2.69A1.84,1.84,0,0,1,408.56,194Z" transform="translate(-12 -11)"/><path class="cls-2" d="M85.56,162.63a2.45,2.45,0,0,1,1.78.73,2.51,2.51,0,0,1,0,3.57,2.47,2.47,0,0,1-1.77.72H84.89a2.45,2.45,0,0,1-1.78-.73,2.51,2.51,0,0,1,0-3.57,2.47,2.47,0,0,1,1.77-.73h0.67Z" transform="translate(-12 -11)"/><path class="cls-2" d="M862,162.63a2.45,2.45,0,0,1,1.78.73,2.51,2.51,0,0,1,0,3.57,2.47,2.47,0,0,1-1.77.72h-0.67a2.45,2.45,0,0,1-1.78-.73,2.51,2.51,0,0,1,0-3.57,2.47,2.47,0,0,1,1.78-.73H862Z" transform="translate(-12 -11)"/><path class="cls-3" d="M341.12,150.58V160H331.7v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M478.13,150.58V160h-9.42v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M199.38,150.69a4.6,4.6,0,1,1-3.24,1.35A4.45,4.45,0,0,1,199.38,150.69Z" transform="translate(-12 -11)"/><path class="cls-3" d="M660.81,109.09v9.41h-9.42v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M523.8,67.61V77h-9.42V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M21.42,67.61V77H12V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M546.63,67.61V77h-9.42V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M797.82,67.61V77H788.4V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M678.93,67.72a4.6,4.6,0,1,1-3.24,1.35,4.45,4.45,0,0,1,3.24-1.35h0Z" transform="translate(-12 -11)"/><path class="cls-2" d="M248.72,69.54l0.72,0.14,0.61,0.42a1.84,1.84,0,0,1,.56,1.36,1.9,1.9,0,0,1-1.9,1.89,1.84,1.84,0,0,1-1.36-.56,1.82,1.82,0,0,1-.56-1.34A1.9,1.9,0,0,1,248.72,69.54Z" transform="translate(-12 -11)"/><path class="cls-3" d="M496.24,26.24a4.6,4.6,0,1,1,0,9.19A4.59,4.59,0,0,1,493,27.59,4.42,4.42,0,0,1,496.24,26.24Z" transform="translate(-12 -11)"/><path class="cls-2" d="M362.89,28.06a1.82,1.82,0,0,1,1.34.56,1.85,1.85,0,0,1,.56,1.36,1.9,1.9,0,0,1-1.9,1.89,1.85,1.85,0,0,1-1.36-.56A1.82,1.82,0,0,1,361,30,1.9,1.9,0,0,1,362.89,28.06Z" transform="translate(-12 -11)"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="552.48" height="226.39" version="1.1" viewBox="0 0 146.18 59.901" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(3.0169 0 0 3.0169 -99.412 -462.64)"><g stroke-width=".40656" aria-label="#ERGO"><path d="m34.33 165.07h1.9027l2.0003-11.351h-1.9027l-0.55292 3.1549h-2.0328v1.7888h1.7075l-0.24394 1.4636h-2.0816v1.7888h1.7563zm3.1549 0h1.9027l0.55292-3.1549h2.0328v-1.7888h-1.7075l0.24393-1.4636h2.0816v-1.7888h-1.7563l0.55292-3.1549h-1.9027z" fill="#5f901d"/><g fill="#161616"><path d="m51.898 165.07v-2.0003h-4.8136v-2.7483h4.651v-2.0003h-4.651v-2.602h4.8136v-2.0003h-7.253v11.351z"/><path d="m56.001 160.86h1.1221l1.9027 4.2119h2.667l-2.2279-4.5372c1.2685-0.35777 2.0003-1.61 2.0003-3.2037 0-2.1954-1.2522-3.6102-3.4801-3.6102h-4.3908v11.351h2.4068zm0-1.8864v-3.285h1.3986c1.1384 0 1.5287 0.40656 1.5287 1.3986v0.48787c0 0.992-0.3903 1.3986-1.5287 1.3986z"/><path d="m68.823 165.07h2.1791v-6.0658h-4.1144v1.7238h1.9352v0.82938c0 1.0083-0.55292 1.7401-1.6425 1.7401-1.4148 0-1.8864-1.2034-1.8864-3.041v-1.8539c0-1.8214 0.45534-2.911 1.6913-2.911 1.1221 0 1.4961 0.89443 1.7401 1.8539l2.2767-0.55291c-0.45534-1.9515-1.6262-3.2687-3.968-3.2687-2.9435 0-4.3258 2.1141-4.3258 5.9683 0 3.6102 1.2034 5.7731 3.4313 5.7731 1.3986 0 2.1629-0.8619 2.5369-1.8051h0.14636z"/><path d="m76.791 165.27c3.0411 0 4.4396-2.1629 4.4396-5.8707 0-3.7078-1.3986-5.8707-4.4396-5.8707-3.041 0-4.4396 2.1629-4.4396 5.8707 0 3.7078 1.3986 5.8707 4.4396 5.8707zm0-1.9677c-1.3823 0-1.8376-1.0896-1.8376-2.911v-1.984c0-1.8214 0.45534-2.911 1.8376-2.911 1.3823 0 1.8376 1.0896 1.8376 2.911v1.9677c0 1.8376-0.45534 2.9272-1.8376 2.9272z"/></g></g><g fill="#4a7411" stroke-width=".17823" aria-label="irc server"><path d="m42.203 168.4c0.24239 0 0.34932-0.12833 0.34932-0.32081v-0.0927c0-0.19249-0.10694-0.32081-0.34932-0.32081s-0.34933 0.12832-0.34933 0.32081v0.0927c0 0.19248 0.10694 0.32081 0.34933 0.32081zm-0.28516 4.5412h0.57033v-3.6786h-0.57033z"/><path d="m44.271 172.94v-2.4952c0-0.34933 0.37071-0.61311 0.98382-0.61311h0.33507v-0.57032h-0.221c-0.59884 0-0.93391 0.32793-1.0622 0.67726h-0.03565v-0.67726h-0.57033v3.6786z"/><path d="m47.65 173.03c0.69865 0 1.1763-0.3422 1.4116-0.86975l-0.41349-0.27804c-0.19961 0.42062-0.53468 0.64162-0.99807 0.64162-0.67726 0-1.0266-0.46339-1.0266-1.105v-0.62736c0-0.64162 0.34932-1.105 1.0266-1.105 0.44913 0 0.76281 0.221 0.89826 0.59885l0.47765-0.24239c-0.21387-0.50617-0.64875-0.86262-1.3759-0.86262-1.0337 0-1.6397 0.74855-1.6397 1.9248s0.60597 1.9249 1.6397 1.9249z"/><path d="m52.655 173.03c0.84123 0 1.3617-0.43488 1.3617-1.1478 0-0.55607-0.31368-0.91252-1.1264-1.0337l-0.28516-0.0428c-0.45626-0.0713-0.70578-0.21387-0.70578-0.57033 0-0.34932 0.24952-0.57032 0.72004-0.57032 0.47052 0 0.7842 0.221 0.94817 0.44913l0.37784-0.3422c-0.29942-0.37071-0.69152-0.59171-1.2832-0.59171-0.74855 0-1.3118 0.35645-1.3118 1.0836 0 0.68439 0.50616 0.96243 1.1834 1.0622l0.29229 0.0428c0.48478 0.0713 0.64162 0.29229 0.64162 0.57745 0 0.37785-0.28516 0.59885-0.76994 0.59885-0.46339 0-0.80559-0.20675-1.0908-0.5632l-0.40636 0.32794c0.32794 0.43487 0.77707 0.72004 1.4543 0.72004z"/><path d="m56.405 173.03c0.69152 0 1.2191-0.3422 1.4543-0.84124l-0.40636-0.29229c-0.19248 0.40636-0.54894 0.63449-1.0123 0.63449-0.68439 0-1.0908-0.47765-1.0908-1.1121v-0.1711h2.6449v-0.2709c0-1.0408-0.60597-1.7965-1.5898-1.7965-0.99807 0-1.6539 0.75568-1.6539 1.9248s0.65588 1.9249 1.6539 1.9249zm0-3.3721c0.58459 0 0.97669 0.43487 0.97669 1.0836v0.0784h-2.0318v-0.0499c0-0.64161 0.43487-1.1121 1.0551-1.1121z"/><path d="m59.506 172.94v-2.4952c0-0.34933 0.37071-0.61311 0.98381-0.61311h0.33507v-0.57032h-0.221c-0.59884 0-0.93391 0.32793-1.0622 0.67726h-0.03565v-0.67726h-0.57033v3.6786z"/><path d="m63.099 172.94 1.2975-3.6786h-0.54894l-0.65588 1.825-0.39923 1.2547h-0.03564l-0.39923-1.2547-0.64162-1.825h-0.57033l1.2904 3.6786z"/><path d="m66.457 173.03c0.69152 0 1.2191-0.3422 1.4543-0.84124l-0.40636-0.29229c-0.19249 0.40636-0.54894 0.63449-1.0123 0.63449-0.68439 0-1.0908-0.47765-1.0908-1.1121v-0.1711h2.6449v-0.2709c0-1.0408-0.60597-1.7965-1.5898-1.7965-0.99807 0-1.6539 0.75568-1.6539 1.9248s0.65588 1.9249 1.6539 1.9249zm0-3.3721c0.58458 0 0.97668 0.43487 0.97668 1.0836v0.0784h-2.0318v-0.0499c0-0.64161 0.43488-1.1121 1.0551-1.1121z"/><path d="m69.558 172.94v-2.4952c0-0.34933 0.37071-0.61311 0.98382-0.61311h0.33507v-0.57032h-0.221c-0.59884 0-0.93391 0.32793-1.0622 0.67726h-0.03565v-0.67726h-0.57033v3.6786z"/></g></g></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.4 KiB |
@ -7,37 +7,37 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/oragono/oragono/irc"
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
"github.com/oragono/oragono/irc/mkcerts"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/ergochat/ergo/irc"
|
||||
"github.com/ergochat/ergo/irc/logger"
|
||||
"github.com/ergochat/ergo/irc/mkcerts"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// set via linker flags, either by make or by goreleaser:
|
||||
var commit = "" // git hash
|
||||
var version = "" // tagged version
|
||||
|
||||
//go:embed default.yaml
|
||||
var defaultConfig string
|
||||
|
||||
// get a password from stdin from the user
|
||||
func getPassword() string {
|
||||
fd := int(os.Stdin.Fd())
|
||||
if terminal.IsTerminal(fd) {
|
||||
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
log.Fatal("Error reading password:", err.Error())
|
||||
}
|
||||
return string(bytePassword)
|
||||
func getPasswordFromTerminal() string {
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
log.Fatal("Error reading password:", err.Error())
|
||||
}
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
return string(bytePassword)
|
||||
}
|
||||
|
||||
func fileDoesNotExist(file string) bool {
|
||||
@ -47,7 +47,7 @@ func fileDoesNotExist(file string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// implements the `oragono mkcerts` command
|
||||
// implements the `ergo mkcerts` command
|
||||
func doMkcerts(configFile string, quiet bool) {
|
||||
config, err := irc.LoadRawConfig(configFile)
|
||||
if err != nil {
|
||||
@ -78,7 +78,7 @@ func doMkcerts(configFile string, quiet bool) {
|
||||
if !(fileDoesNotExist(cert) && fileDoesNotExist(key)) {
|
||||
log.Fatalf("Preexisting TLS cert and/or key files: %s %s", cert, key)
|
||||
}
|
||||
err := mkcerts.CreateCert("Oragono", host, cert, key)
|
||||
err := mkcerts.CreateCert("Ergo", host, cert, key)
|
||||
if err == nil {
|
||||
if !quiet {
|
||||
log.Printf(" Certificate created at %s : %s\n", cert, key)
|
||||
@ -92,16 +92,18 @@ func doMkcerts(configFile string, quiet bool) {
|
||||
|
||||
func main() {
|
||||
irc.SetVersionString(version, commit)
|
||||
usage := `oragono.
|
||||
usage := `ergo.
|
||||
Usage:
|
||||
oragono initdb [--conf <filename>] [--quiet]
|
||||
oragono upgradedb [--conf <filename>] [--quiet]
|
||||
oragono importdb <database.json> [--conf <filename>] [--quiet]
|
||||
oragono genpasswd [--conf <filename>] [--quiet]
|
||||
oragono mkcerts [--conf <filename>] [--quiet]
|
||||
oragono run [--conf <filename>] [--quiet] [--smoke]
|
||||
oragono -h | --help
|
||||
oragono --version
|
||||
ergo initdb [--conf <filename>] [--quiet]
|
||||
ergo upgradedb [--conf <filename>] [--quiet]
|
||||
ergo importdb <database.json> [--conf <filename>] [--quiet]
|
||||
ergo genpasswd [--conf <filename>] [--quiet]
|
||||
ergo mkcerts [--conf <filename>] [--quiet]
|
||||
ergo defaultconfig
|
||||
ergo gentoken
|
||||
ergo run [--conf <filename>] [--quiet] [--smoke]
|
||||
ergo -h | --help
|
||||
ergo --version
|
||||
Options:
|
||||
--conf <filename> Configuration file to use [default: ircd.yaml].
|
||||
--quiet Don't show startup/shutdown lines.
|
||||
@ -113,28 +115,36 @@ Options:
|
||||
// don't require a config file for genpasswd
|
||||
if arguments["genpasswd"].(bool) {
|
||||
var password string
|
||||
fd := int(os.Stdin.Fd())
|
||||
if terminal.IsTerminal(fd) {
|
||||
if term.IsTerminal(int(syscall.Stdin)) {
|
||||
fmt.Print("Enter Password: ")
|
||||
password = getPassword()
|
||||
password = getPasswordFromTerminal()
|
||||
fmt.Print("\n")
|
||||
fmt.Print("Reenter Password: ")
|
||||
confirm := getPassword()
|
||||
confirm := getPasswordFromTerminal()
|
||||
fmt.Print("\n")
|
||||
if confirm != password {
|
||||
log.Fatal("passwords do not match")
|
||||
}
|
||||
} else {
|
||||
password = getPassword()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
password = strings.TrimSpace(text)
|
||||
}
|
||||
if err := irc.ValidatePassphrase(password); err != nil {
|
||||
log.Printf("WARNING: this password contains characters that may cause problems with your IRC client software.\n")
|
||||
log.Printf("We strongly recommend choosing a different password.\n")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
log.Fatal("encoding error:", err.Error())
|
||||
}
|
||||
fmt.Print(string(hash))
|
||||
if terminal.IsTerminal(fd) {
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Println(string(hash))
|
||||
return
|
||||
} else if arguments["defaultconfig"].(bool) {
|
||||
fmt.Print(defaultConfig)
|
||||
return
|
||||
} else if arguments["gentoken"].(bool) {
|
||||
fmt.Println(utils.GenerateSecretKey())
|
||||
return
|
||||
} else if arguments["mkcerts"].(bool) {
|
||||
doMkcerts(arguments["--conf"].(string), arguments["--quiet"].(bool))
|
||||
@ -183,7 +193,7 @@ Options:
|
||||
|
||||
// warning if running a non-final version
|
||||
if strings.Contains(irc.Ver, "unreleased") {
|
||||
logman.Warning("server", "You are currently running an unreleased beta version of Oragono that may be unstable and could corrupt your database.\nIf you are running a production network, please download the latest build from https://oragono.io/downloads.html and run that instead.")
|
||||
logman.Warning("server", "You are currently running an unreleased beta version of Ergo that may be unstable and could corrupt your database.\nIf you are running a production network, please download the latest build from https://ergo.chat/about and run that instead.")
|
||||
}
|
||||
|
||||
server, err := irc.NewServer(config, logman)
|
||||
@ -191,10 +201,6 @@ Options:
|
||||
logman.Error("server", fmt.Sprintf("Could not load server: %s", err.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
if !arguments["--quiet"].(bool) {
|
||||
logman.Info("server", "Server running")
|
||||
defer logman.Info("server", fmt.Sprintf("Oragono v%s exiting", irc.SemVer))
|
||||
}
|
||||
if !arguments["--smoke"].(bool) {
|
||||
server.Run()
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
__ __ ______ ___ ______ ___
|
||||
__/ // /_/ ____/ __ \/ ____/ __ \
|
||||
/_ // __/ __/ / /_/ / / __/ / / /
|
||||
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
||||
/_//_/ /_____/_/ |_|\____/\____/
|
||||
|
||||
▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
|
||||
▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
|
||||
▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄
|
||||
▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌
|
||||
▀█▄▀▪.▀ ▀ ▀ ▀ ·▀▀▀▀ ▀█▄▀ ▀▀ █▪ ▀█▄▀▪
|
||||
|
||||
This is the default Oragono MOTD.
|
||||
This is the default Ergo MOTD.
|
||||
|
||||
|
||||
If motd-formatting is enabled in the config file, you can use the dollarsign character to
|
||||
@ -63,6 +63,12 @@ CAPDEFS = [
|
||||
url="https://ircv3.net/specs/extensions/extended-join-3.1.html",
|
||||
standard="IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="ExtendedMonitor",
|
||||
name="extended-monitor",
|
||||
url="https://ircv3.net/specs/extensions/extended-monitor.html",
|
||||
standard="IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="InviteNotify",
|
||||
name="invite-notify",
|
||||
@ -81,6 +87,12 @@ CAPDEFS = [
|
||||
url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="MessageRedaction",
|
||||
name="draft/message-redaction",
|
||||
url="https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="MessageTags",
|
||||
name="message-tags",
|
||||
@ -105,12 +117,6 @@ CAPDEFS = [
|
||||
url="https://ircv3.net/specs/extensions/channel-rename",
|
||||
standard="draft IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Resume",
|
||||
name="draft/resume-0.5",
|
||||
url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="SASL",
|
||||
name="sasl",
|
||||
@ -161,9 +167,9 @@ CAPDEFS = [
|
||||
),
|
||||
CapDef(
|
||||
identifier="Nope",
|
||||
name="oragono.io/nope",
|
||||
url="https://oragono.io/nope",
|
||||
standard="Oragono vendor",
|
||||
name="ergo.chat/nope",
|
||||
url="https://ergo.chat/nope",
|
||||
standard="Ergo vendor",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Multiline",
|
||||
@ -178,11 +184,66 @@ CAPDEFS = [
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Register",
|
||||
name="draft/register",
|
||||
url="https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495",
|
||||
identifier="AccountRegistration",
|
||||
name="draft/account-registration",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/435",
|
||||
standard="draft IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="ReadMarker",
|
||||
name="draft/read-marker",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/489",
|
||||
standard="draft IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Persistence",
|
||||
name="draft/persistence",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/503",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Preaway",
|
||||
name="draft/pre-away",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/514",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="StandardReplies",
|
||||
name="standard-replies",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/506",
|
||||
standard="IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="NoImplicitNames",
|
||||
name="draft/no-implicit-names",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/527",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="ExtendedISupport",
|
||||
name="draft/extended-isupport",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/543",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="WebPush",
|
||||
name="draft/webpush",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/471",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="SojuWebPush",
|
||||
name="soju.im/webpush",
|
||||
url="https://github.com/ircv3/ircv3-specifications/pull/471",
|
||||
standard="Soju/Goguma vendor",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Metadata",
|
||||
name="draft/metadata-2",
|
||||
url="https://ircv3.net/specs/extensions/metadata",
|
||||
standard="draft IRCv3",
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
def validate_defs():
|
||||
@ -218,7 +279,7 @@ package caps
|
||||
const (
|
||||
// number of recognized capabilities:
|
||||
numCapabs = %d
|
||||
// length of the uint64 array that represents the bitset:
|
||||
// length of the uint32 array that represents the bitset:
|
||||
bitsetLen = %d
|
||||
)
|
||||
""" % (numCapabs, bitsetLen), file=output)
|
||||
|
||||
50
go.mod
50
go.mod
@ -1,25 +1,47 @@
|
||||
module github.com/oragono/oragono
|
||||
module github.com/ergochat/ergo
|
||||
|
||||
go 1.15
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-test/deep v1.0.6 // indirect
|
||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
||||
github.com/ergochat/irc-go v0.5.0-rc2
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/goshuirc/irc-go v0.0.0-20210108124156-ec778d0252a5
|
||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
||||
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||
github.com/onsi/gomega v1.9.0 // indirect
|
||||
github.com/oragono/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
||||
github.com/oragono/go-ident v0.0.0-20200511222032-830550b1d775
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
github.com/tidwall/buntdb v1.1.4
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf // indirect
|
||||
golang.org/x/text v0.3.4
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
github.com/tidwall/buntdb v1.3.2
|
||||
github.com/xdg-go/scram v1.0.2
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.25.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/emersion/go-msgauth v0.7.0
|
||||
github.com/ergochat/webpush-go/v2 v2.0.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tidwall/btree v1.4.2 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/tidwall/grect v0.1.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/rtred v0.1.2 // indirect
|
||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
|
||||
|
||||
replace github.com/xdg-go/scram => github.com/ergochat/scram v1.0.2-ergo1
|
||||
|
||||
129
go.sum
129
go.sum
@ -1,88 +1,99 @@
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE=
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
||||
github.com/DanielOaks/go-idn v0.0.0-20160120021903-76db0e10dc65/go.mod h1:GYIaL2hleNQvfMUBTes1Zd/lDTyI/p2hv3kYB4jssyU=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=
|
||||
github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
|
||||
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
|
||||
github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck=
|
||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
|
||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
|
||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
|
||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
|
||||
github.com/ergochat/irc-go v0.5.0-rc2 h1:VuSQJF5K4hWvYSzGa4b8vgL6kzw8HF6LSOejE+RWpAo=
|
||||
github.com/ergochat/irc-go v0.5.0-rc2/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
||||
github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
|
||||
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/ergochat/webpush-go/v2 v2.0.0 h1:n6eoJk8RpzJFeBJ6gxvqo/dngnVEmJbzJwzKtCZbByo=
|
||||
github.com/ergochat/webpush-go/v2 v2.0.0/go.mod h1:OQlhnq8JeHDzRzAy6bdDObr19uqbHliOV+z7mHbYr4c=
|
||||
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=
|
||||
github.com/ergochat/websocket v1.4.2-oragono1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/goshuirc/eventmgr v0.0.0-20170615162049-060479027c93/go.mod h1:bjJFM4iZJWTf9Rka9sNuI3GxszJqFeu5r1r15ZVtemo=
|
||||
github.com/goshuirc/irc-go v0.0.0-20201116034710-7e7b0985c4b5 h1:oqOT5hi8MRDGvfu4h7rlsrtper7I/0A41K0GppmBb5w=
|
||||
github.com/goshuirc/irc-go v0.0.0-20201116034710-7e7b0985c4b5/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
|
||||
github.com/goshuirc/irc-go v0.0.0-20201118022549-7209d10d54a8 h1:7vZqkY9bwimFNuLhWAzdxM9IM7ym853YLNhWsKAnsrQ=
|
||||
github.com/goshuirc/irc-go v0.0.0-20201118022549-7209d10d54a8/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
|
||||
github.com/goshuirc/irc-go v0.0.0-20201228002532-4e36cb3f41f1 h1:Kyyey3K8nhx60lt4xish6NzLqButwqAwDb62UOU3GbE=
|
||||
github.com/goshuirc/irc-go v0.0.0-20201228002532-4e36cb3f41f1/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
|
||||
github.com/goshuirc/irc-go v0.0.0-20210108124156-ec778d0252a5 h1:TXGvyYHJEBluqwI8d0V5/QmSnNxEYIMbfPE36B8CNK8=
|
||||
github.com/goshuirc/irc-go v0.0.0-20210108124156-ec778d0252a5/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd h1:+iAPaTbi1gZpcpDwe/BW1fx7Xoesv69hLNGPheoyhBs=
|
||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd/go.mod h1:4soZNh0zW0LtYGdQ416i0jO0EIqMGcbtaspRS4BDvRQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
|
||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
|
||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/oragono/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:Y87UllAzZJfDbiHTEo9TEiw+YxoW++tGFkkd0Nndkjc=
|
||||
github.com/oragono/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:+uesPRay9e5tW6zhw4CJkRV3QOEbbZIJcsuo9ZnC+hE=
|
||||
github.com/oragono/go-ident v0.0.0-20200511222032-830550b1d775 h1:AMAsAn/i4AgsmWQYdMoze9omwtHpbxrKuT+AT1LmhtI=
|
||||
github.com/oragono/go-ident v0.0.0-20200511222032-830550b1d775/go.mod h1:r5Fk840a4eu3ii1kxGDNSJupQu9Z1UC1nfJOZZXC24c=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tidwall/btree v0.2.2 h1:VVo0JW/tdidNdQzNsDR4wMbL3heaxA1DGleyzQ3/niY=
|
||||
github.com/tidwall/btree v0.2.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
|
||||
github.com/tidwall/buntdb v1.1.4 h1:W7y9+2dM3GOswU0t3pz6+BcwZXjj/tVOhPcO6EHufME=
|
||||
github.com/tidwall/buntdb v1.1.4/go.mod h1:06+/n7EFf6uUaIG5r9xZcExYN3H0Lnc+g/Kqx0fZFkI=
|
||||
github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws=
|
||||
github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
|
||||
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
|
||||
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/rtree v0.0.0-20201027154624-32188eeb08a8 h1:BsKSRhu0TDB6Snq8SutN9KQHc6vqHEXJTcAFwyGNius=
|
||||
github.com/tidwall/rtree v0.0.0-20201027154624-32188eeb08a8/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
|
||||
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
|
||||
github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g=
|
||||
github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE=
|
||||
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
|
||||
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
|
||||
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
|
||||
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
|
||||
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
|
||||
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
|
||||
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
|
||||
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf h1:kt3wY1Lu5MJAnKTfoMR52Cu4gwvna4VTzNOiT8tY73s=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -94,5 +105,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
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=
|
||||
|
||||
76
irc/accept.go
Normal file
76
irc/accept.go
Normal file
@ -0,0 +1,76 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// tracks ACCEPT relationships, i.e., `accepter` is willing to receive DMs from
|
||||
// `accepted` despite some restriction (currently the only relevant restriction
|
||||
// is that `accepter` is +R and `accepted` is not logged in)
|
||||
|
||||
type AcceptManager struct {
|
||||
sync.RWMutex
|
||||
|
||||
// maps recipient -> whitelist of permitted senders:
|
||||
// this is what we actually check
|
||||
clientToAccepted map[*Client]utils.HashSet[*Client]
|
||||
// this is the reverse mapping, it's needed so we can
|
||||
// clean up the forward mapping during (*Client).destroy():
|
||||
clientToAccepters map[*Client]utils.HashSet[*Client]
|
||||
}
|
||||
|
||||
func (am *AcceptManager) Initialize() {
|
||||
am.clientToAccepted = make(map[*Client]utils.HashSet[*Client])
|
||||
am.clientToAccepters = make(map[*Client]utils.HashSet[*Client])
|
||||
}
|
||||
|
||||
func (am *AcceptManager) MaySendTo(sender, recipient *Client) (result bool) {
|
||||
am.RLock()
|
||||
defer am.RUnlock()
|
||||
return am.clientToAccepted[recipient].Has(sender)
|
||||
}
|
||||
|
||||
func (am *AcceptManager) Accept(accepter, accepted *Client) {
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
|
||||
var m utils.HashSet[*Client]
|
||||
|
||||
m = am.clientToAccepted[accepter]
|
||||
if m == nil {
|
||||
m = make(utils.HashSet[*Client])
|
||||
am.clientToAccepted[accepter] = m
|
||||
}
|
||||
m.Add(accepted)
|
||||
|
||||
m = am.clientToAccepters[accepted]
|
||||
if m == nil {
|
||||
m = make(utils.HashSet[*Client])
|
||||
am.clientToAccepters[accepted] = m
|
||||
}
|
||||
m.Add(accepter)
|
||||
}
|
||||
|
||||
func (am *AcceptManager) Unaccept(accepter, accepted *Client) {
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
|
||||
delete(am.clientToAccepted[accepter], accepted)
|
||||
delete(am.clientToAccepters[accepted], accepter)
|
||||
}
|
||||
|
||||
func (am *AcceptManager) Remove(client *Client) {
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
|
||||
for accepter := range am.clientToAccepters[client] {
|
||||
delete(am.clientToAccepted[accepter], client)
|
||||
}
|
||||
for accepted := range am.clientToAccepted[client] {
|
||||
delete(am.clientToAccepters[accepted], client)
|
||||
}
|
||||
delete(am.clientToAccepters, client)
|
||||
delete(am.clientToAccepted, client)
|
||||
}
|
||||
108
irc/accept_test.go
Normal file
108
irc/accept_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccept(t *testing.T) {
|
||||
var am AcceptManager
|
||||
am.Initialize()
|
||||
|
||||
alice := new(Client)
|
||||
bob := new(Client)
|
||||
eve := new(Client)
|
||||
|
||||
// must not panic:
|
||||
am.Unaccept(eve, bob)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), false)
|
||||
assertEqual(am.MaySendTo(bob, alice), false)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), false)
|
||||
|
||||
am.Accept(alice, bob)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), false)
|
||||
assertEqual(am.MaySendTo(bob, alice), true)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), false)
|
||||
|
||||
am.Accept(bob, alice)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), true)
|
||||
assertEqual(am.MaySendTo(bob, alice), true)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), false)
|
||||
|
||||
am.Accept(bob, eve)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), true)
|
||||
assertEqual(am.MaySendTo(bob, alice), true)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), true)
|
||||
|
||||
am.Accept(eve, bob)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), true)
|
||||
assertEqual(am.MaySendTo(bob, alice), true)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), true)
|
||||
assertEqual(am.MaySendTo(eve, bob), true)
|
||||
|
||||
am.Unaccept(eve, bob)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), true)
|
||||
assertEqual(am.MaySendTo(bob, alice), true)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), true)
|
||||
|
||||
am.Remove(alice)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), false)
|
||||
assertEqual(am.MaySendTo(bob, alice), false)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), true)
|
||||
|
||||
am.Remove(bob)
|
||||
|
||||
assertEqual(am.MaySendTo(alice, bob), false)
|
||||
assertEqual(am.MaySendTo(bob, alice), false)
|
||||
assertEqual(am.MaySendTo(alice, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, alice), false)
|
||||
assertEqual(am.MaySendTo(bob, eve), false)
|
||||
assertEqual(am.MaySendTo(eve, bob), false)
|
||||
}
|
||||
|
||||
func TestAcceptInternal(t *testing.T) {
|
||||
var am AcceptManager
|
||||
am.Initialize()
|
||||
|
||||
alice := new(Client)
|
||||
bob := new(Client)
|
||||
eve := new(Client)
|
||||
|
||||
am.Accept(alice, bob)
|
||||
am.Accept(bob, alice)
|
||||
am.Accept(bob, eve)
|
||||
am.Remove(alice)
|
||||
am.Remove(bob)
|
||||
|
||||
// assert that there is no memory leak
|
||||
for _, client := range []*Client{alice, bob, eve} {
|
||||
assertEqual(len(am.clientToAccepted[client]), 0)
|
||||
assertEqual(len(am.clientToAccepters[client]), 0)
|
||||
}
|
||||
}
|
||||
653
irc/accounts.go
653
irc/accounts.go
@ -4,7 +4,8 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@ -15,20 +16,23 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/email"
|
||||
"github.com/oragono/oragono/irc/migrations"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/irc-go/ircutils"
|
||||
"github.com/tidwall/buntdb"
|
||||
"github.com/xdg-go/scram"
|
||||
|
||||
"github.com/ergochat/ergo/irc/connection_limits"
|
||||
"github.com/ergochat/ergo/irc/email"
|
||||
"github.com/ergochat/ergo/irc/migrations"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/oauth2"
|
||||
"github.com/ergochat/ergo/irc/passwd"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
keyAccountExists = "account.exists %s"
|
||||
keyAccountVerified = "account.verified %s"
|
||||
keyAccountUnregistered = "account.unregistered %s"
|
||||
keyAccountCallback = "account.callback %s"
|
||||
keyAccountVerificationCode = "account.verificationcode %s"
|
||||
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
@ -37,14 +41,18 @@ const (
|
||||
keyAccountSettings = "account.settings %s"
|
||||
keyAccountVHost = "account.vhost %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||
keyAccountLastSeen = "account.lastseen %s"
|
||||
keyAccountReadMarkers = "account.readmarkers %s"
|
||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||
keyAccountRealname = "account.realname %s" // client realname stored as string
|
||||
keyAccountSuspended = "account.suspended %s" // client realname stored as string
|
||||
keyAccountPwReset = "account.pwreset %s"
|
||||
keyAccountEmailChange = "account.emailchange %s"
|
||||
// for an always-on client, a map of channel names they're in to their current modes
|
||||
// (not to be confused with their amodes, which a non-always-on client can have):
|
||||
keyAccountChannelToModes = "account.channeltomodes %s"
|
||||
keyAccountChannelToModes = "account.channeltomodes %s"
|
||||
keyAccountPushSubscriptions = "account.pushsubscriptions %s"
|
||||
keyAccountMetadata = "account.metadata %s"
|
||||
|
||||
maxCertfpsPerAccount = 5
|
||||
)
|
||||
@ -125,9 +133,12 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) {
|
||||
am.server.AddAlwaysOnClient(
|
||||
account,
|
||||
am.loadChannels(accountName),
|
||||
am.loadLastSeen(accountName),
|
||||
am.loadTimeMap(keyAccountLastSeen, accountName),
|
||||
am.loadTimeMap(keyAccountReadMarkers, accountName),
|
||||
am.loadModes(accountName),
|
||||
am.loadRealname(accountName),
|
||||
am.loadPushSubscriptions(accountName),
|
||||
am.loadMetadata(accountName),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -173,13 +184,13 @@ func (am *AccountManager) buildNickToAccountIndex(config *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil {
|
||||
if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil && rawPrefs != "" {
|
||||
var prefs AccountSettings
|
||||
err := json.Unmarshal([]byte(rawPrefs), &prefs)
|
||||
if err == nil && prefs.NickEnforcement != NickEnforcementOptional {
|
||||
accountToMethod[account] = prefs.NickEnforcement
|
||||
} else if err != nil {
|
||||
am.server.logger.Error("internal", "corrupt account creds", account)
|
||||
am.server.logger.Error("internal", "corrupt account settings", account, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,7 +363,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
config := am.server.Config()
|
||||
|
||||
// final "is registration allowed" check:
|
||||
if !(config.Accounts.Registration.Enabled || callbackNamespace == "admin") || am.server.Defcon() <= 4 {
|
||||
if callbackNamespace != "admin" && (!config.Accounts.Registration.Enabled || am.server.Defcon() <= 4) {
|
||||
return errFeatureDisabled
|
||||
}
|
||||
|
||||
@ -387,10 +398,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
var creds AccountCredentials
|
||||
@ -405,8 +416,16 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
return err
|
||||
}
|
||||
|
||||
var settingsStr string
|
||||
if callbackNamespace == "mailto" {
|
||||
settings := AccountSettings{Email: callbackValue}
|
||||
j, err := json.Marshal(settings)
|
||||
if err == nil {
|
||||
settingsStr = string(j)
|
||||
}
|
||||
}
|
||||
|
||||
registeredTimeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
|
||||
|
||||
var setOptions *buntdb.SetOptions
|
||||
ttl := time.Duration(config.Accounts.Registration.VerifyTimeout)
|
||||
@ -420,7 +439,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
|
||||
// can't register an account with the same name as a registered nick
|
||||
if am.NickToAccount(account) != "" {
|
||||
return errAccountAlreadyRegistered
|
||||
return errNameReserved
|
||||
}
|
||||
|
||||
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
@ -445,7 +464,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
tx.Set(accountNameKey, account, setOptions)
|
||||
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
||||
tx.Set(credentialsKey, credStr, setOptions)
|
||||
tx.Set(callbackKey, callbackSpec, setOptions)
|
||||
tx.Set(settingsKey, settingsStr, setOptions)
|
||||
if certfp != "" {
|
||||
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
||||
}
|
||||
@ -460,8 +479,12 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue)
|
||||
if err != nil {
|
||||
am.Unregister(casefoldedAccount, true)
|
||||
return errCallbackFailed
|
||||
return ®istrationCallbackError{underlying: err}
|
||||
} else {
|
||||
if client != nil && code != "" {
|
||||
am.server.logger.Info("accounts",
|
||||
fmt.Sprintf("nickname %s registered account %s, pending verification", client.Nick(), account))
|
||||
}
|
||||
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err = tx.Set(verificationCodeKey, code, setOptions)
|
||||
return err
|
||||
@ -469,8 +492,30 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
}
|
||||
}
|
||||
|
||||
// validatePassphrase checks whether a passphrase is allowed by our rules
|
||||
func validatePassphrase(passphrase string) error {
|
||||
type registrationCallbackError struct {
|
||||
underlying error
|
||||
}
|
||||
|
||||
func (r *registrationCallbackError) Error() string {
|
||||
return `Account verification could not be sent`
|
||||
}
|
||||
|
||||
func registrationCallbackErrorText(config *Config, client *Client, err error) string {
|
||||
if callbackErr, ok := err.(*registrationCallbackError); ok {
|
||||
// only expose a user-visible error if we are doing direct sending
|
||||
if config.Accounts.Registration.EmailVerification.DirectSendingEnabled() {
|
||||
errorText := ircutils.SanitizeText(callbackErr.underlying.Error(), 350)
|
||||
return fmt.Sprintf(client.t("Could not dispatch registration e-mail: %s"), errorText)
|
||||
} else {
|
||||
return client.t("Could not dispatch registration e-mail")
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePassphrase checks whether a passphrase is allowed by our rules
|
||||
func ValidatePassphrase(passphrase string) error {
|
||||
// sanity check the length
|
||||
if len(passphrase) == 0 || len(passphrase) > 300 {
|
||||
return errAccountBadPassphrase
|
||||
@ -479,7 +524,11 @@ func validatePassphrase(passphrase string) error {
|
||||
if passphrase == "*" {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
// for now, just enforce that spaces are not allowed
|
||||
// validate that the passphrase contains no spaces, and furthermore is valid as a
|
||||
// non-final IRC parameter. we already checked that it is nonempty:
|
||||
if passphrase[0] == ':' {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
for _, r := range passphrase {
|
||||
if unicode.IsSpace(r) {
|
||||
return errAccountBadPassphrase
|
||||
@ -489,8 +538,8 @@ func validatePassphrase(passphrase string) error {
|
||||
}
|
||||
|
||||
// changes the password for an account
|
||||
func (am *AccountManager) setPassword(account string, password string, hasPrivs bool) (err error) {
|
||||
cfAccount, err := CasefoldName(account)
|
||||
func (am *AccountManager) setPassword(accountName string, password string, hasPrivs bool) (err error) {
|
||||
cfAccount, err := CasefoldName(accountName)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
@ -605,12 +654,21 @@ func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
|
||||
|
||||
func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
|
||||
key := fmt.Sprintf(keyAccountLastSeen, account)
|
||||
am.saveTimeMap(account, key, lastSeen)
|
||||
}
|
||||
|
||||
func (am *AccountManager) saveReadMarkers(account string, readMarkers map[string]time.Time) {
|
||||
key := fmt.Sprintf(keyAccountReadMarkers, account)
|
||||
am.saveTimeMap(account, key, readMarkers)
|
||||
}
|
||||
|
||||
func (am *AccountManager) saveTimeMap(account, key string, timeMap map[string]time.Time) {
|
||||
var val string
|
||||
if len(lastSeen) != 0 {
|
||||
text, _ := json.Marshal(lastSeen)
|
||||
if len(timeMap) != 0 {
|
||||
text, _ := json.Marshal(timeMap)
|
||||
val = string(text)
|
||||
}
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
err := am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
if val != "" {
|
||||
tx.Set(key, val, nil)
|
||||
} else {
|
||||
@ -618,10 +676,13 @@ func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "error persisting timeMap", key, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadLastSeen(account string) (lastSeen map[string]time.Time) {
|
||||
key := fmt.Sprintf(keyAccountLastSeen, account)
|
||||
func (am *AccountManager) loadTimeMap(baseKey, account string) (lastSeen map[string]time.Time) {
|
||||
key := fmt.Sprintf(baseKey, account)
|
||||
var lsText string
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
lsText, _ = tx.Get(key)
|
||||
@ -658,6 +719,74 @@ func (am *AccountManager) loadRealname(account string) (realname string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) savePushSubscriptions(account string, subs []storedPushSubscription) {
|
||||
j, err := json.Marshal(subs)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "error storing push subscriptions", err.Error())
|
||||
return
|
||||
}
|
||||
val := string(j)
|
||||
key := fmt.Sprintf(keyAccountPushSubscriptions, account)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(key, val, nil)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadPushSubscriptions(account string) (result []storedPushSubscription) {
|
||||
key := fmt.Sprintf(keyAccountPushSubscriptions, account)
|
||||
var val string
|
||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
val, _ = tx.Get(key)
|
||||
return nil
|
||||
})
|
||||
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(val), &result); err == nil {
|
||||
return result
|
||||
} else {
|
||||
am.server.logger.Error("internal", "error loading push subscriptions", err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) saveMetadata(account string, metadata map[string]string) {
|
||||
j, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "error storing metadata", err.Error())
|
||||
return
|
||||
}
|
||||
val := string(j)
|
||||
key := fmt.Sprintf(keyAccountMetadata, account)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(key, val, nil)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadMetadata(account string) (result map[string]string) {
|
||||
key := fmt.Sprintf(keyAccountMetadata, account)
|
||||
var val string
|
||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
val, _ = tx.Get(key)
|
||||
return nil
|
||||
})
|
||||
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(val), &result); err == nil {
|
||||
return result
|
||||
} else {
|
||||
am.server.logger.Error("internal", "error loading metadata", err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
||||
certfp, err = utils.NormalizeCertfp(certfp)
|
||||
if err != nil {
|
||||
@ -749,15 +878,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, account string,
|
||||
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
||||
}
|
||||
|
||||
var message bytes.Buffer
|
||||
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
|
||||
fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
|
||||
if config.DKIM.Domain != "" {
|
||||
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
|
||||
}
|
||||
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
|
||||
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
|
||||
message.WriteString("\r\n") // blank line: end headers, begin message body
|
||||
message := email.ComposeMail(config, callbackValue, subject)
|
||||
fmt.Fprintf(&message, client.t("Account: %s"), account)
|
||||
message.WriteString("\r\n")
|
||||
fmt.Fprintf(&message, client.t("Verification code: %s"), code)
|
||||
@ -774,7 +895,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, account string,
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) Verify(client *Client, account string, code string) error {
|
||||
func (am *AccountManager) Verify(client *Client, account string, code string, admin bool) error {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
var skeleton string
|
||||
if err != nil || account == "" || account == "*" {
|
||||
@ -790,8 +911,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
|
||||
var raw rawClientAccount
|
||||
|
||||
@ -837,18 +958,20 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
return errAccountAlreadyVerified
|
||||
}
|
||||
|
||||
// actually verify the code
|
||||
// a stored code of "" means a none callback / no code required
|
||||
success := false
|
||||
storedCode, err := tx.Get(verificationCodeKey)
|
||||
if err == nil {
|
||||
// this is probably unnecessary
|
||||
if storedCode == "" || utils.SecretTokensMatch(storedCode, code) {
|
||||
success = true
|
||||
if !admin {
|
||||
// actually verify the code
|
||||
// a stored code of "" means a none callback / no code required
|
||||
success := false
|
||||
storedCode, err := tx.Get(verificationCodeKey)
|
||||
if err == nil {
|
||||
// this is probably unnecessary
|
||||
if storedCode == "" || utils.SecretTokensMatch(storedCode, code) {
|
||||
success = true
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
return errAccountVerificationInvalidCode
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
return errAccountVerificationInvalidCode
|
||||
}
|
||||
|
||||
// verify the account
|
||||
@ -859,8 +982,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
tx.Set(accountKey, "1", nil)
|
||||
tx.Set(accountNameKey, raw.Name, nil)
|
||||
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
|
||||
tx.Set(callbackKey, raw.Callback, nil)
|
||||
tx.Set(credentialsKey, raw.Credentials, nil)
|
||||
tx.Set(settingsKey, raw.Settings, nil)
|
||||
|
||||
var creds AccountCredentials
|
||||
// XXX we shouldn't do (de)serialization inside the txn,
|
||||
@ -899,7 +1022,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
if client != nil {
|
||||
am.Login(client, clientAccount)
|
||||
if client.AlwaysOn() {
|
||||
client.markDirty(IncludeRealname)
|
||||
client.markDirty(IncludeAllAttrs)
|
||||
}
|
||||
}
|
||||
// we may need to do nick enforcement here:
|
||||
@ -917,11 +1040,219 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
func (am *AccountManager) SARegister(account, passphrase string) (err error) {
|
||||
err = am.Register(nil, account, "admin", "", passphrase, "")
|
||||
if err == nil {
|
||||
err = am.Verify(nil, account, "")
|
||||
err = am.Verify(nil, account, "", true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type EmailChangeRecord struct {
|
||||
TimeCreated time.Time
|
||||
Code string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (am *AccountManager) NsSetEmail(client *Client, emailAddr string) (err error) {
|
||||
casefoldedAccount := client.Account()
|
||||
if casefoldedAccount == "" {
|
||||
return errAccountNotLoggedIn
|
||||
}
|
||||
|
||||
if am.touchRegisterThrottle() {
|
||||
am.server.logger.Warning("accounts", "global registration throttle exceeded by client changing email", client.Nick())
|
||||
return errLimitExceeded
|
||||
}
|
||||
|
||||
config := am.server.Config()
|
||||
if !config.Accounts.Registration.EmailVerification.Enabled {
|
||||
return errFeatureDisabled // redundant check, just in case
|
||||
}
|
||||
record := EmailChangeRecord{
|
||||
TimeCreated: time.Now().UTC(),
|
||||
Code: utils.GenerateSecretToken(),
|
||||
Email: emailAddr,
|
||||
}
|
||||
recordKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
||||
recordBytes, _ := json.Marshal(record)
|
||||
recordVal := string(recordBytes)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(recordKey, recordVal, nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := email.ComposeMail(config.Accounts.Registration.EmailVerification,
|
||||
emailAddr,
|
||||
fmt.Sprintf(client.t("Verify your change of e-mail address on %s"), am.server.name))
|
||||
message.WriteString(fmt.Sprintf(client.t("To confirm your change of e-mail address on %s, issue the following command:"), am.server.name))
|
||||
message.WriteString("\r\n")
|
||||
fmt.Fprintf(&message, "/MSG NickServ VERIFYEMAIL %s\r\n", record.Code)
|
||||
|
||||
err = email.SendMail(config.Accounts.Registration.EmailVerification, emailAddr, message.Bytes())
|
||||
if err == nil {
|
||||
am.server.logger.Info("services",
|
||||
fmt.Sprintf("email change verification sent for account %s", casefoldedAccount))
|
||||
return
|
||||
} else {
|
||||
am.server.logger.Error("internal", "Failed to dispatch e-mail change verification to", emailAddr, err.Error())
|
||||
return ®istrationCallbackError{err}
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) NsVerifyEmail(client *Client, code string) (err error) {
|
||||
casefoldedAccount := client.Account()
|
||||
if casefoldedAccount == "" {
|
||||
return errAccountNotLoggedIn
|
||||
}
|
||||
|
||||
var record EmailChangeRecord
|
||||
success := false
|
||||
key := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
||||
ttl := time.Duration(am.server.Config().Accounts.Registration.VerifyTimeout)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
rawStr, err := tx.Get(key)
|
||||
if err == nil && rawStr != "" {
|
||||
err := json.Unmarshal([]byte(rawStr), &record)
|
||||
if err == nil {
|
||||
if (ttl == 0 || time.Since(record.TimeCreated) < ttl) && utils.SecretTokensMatch(record.Code, code) {
|
||||
success = true
|
||||
tx.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if !success {
|
||||
return errAccountVerificationInvalidCode
|
||||
}
|
||||
|
||||
munger := func(in AccountSettings) (out AccountSettings, err error) {
|
||||
out = in
|
||||
out.Email = record.Email
|
||||
return
|
||||
}
|
||||
|
||||
_, err = am.ModifyAccountSettings(casefoldedAccount, munger)
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) NsSendpass(client *Client, accountName string) (err error) {
|
||||
config := am.server.Config()
|
||||
if !(config.Accounts.Registration.EmailVerification.Enabled && config.Accounts.Registration.EmailVerification.PasswordReset.Enabled) {
|
||||
return errFeatureDisabled
|
||||
}
|
||||
|
||||
account, err := am.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !account.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
if account.Suspended != nil {
|
||||
return errAccountSuspended
|
||||
}
|
||||
if account.Settings.Email == "" {
|
||||
return errValidEmailRequired
|
||||
}
|
||||
|
||||
record := PasswordResetRecord{
|
||||
TimeCreated: time.Now().UTC(),
|
||||
Code: utils.GenerateSecretToken(),
|
||||
}
|
||||
recordKey := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
|
||||
recordBytes, _ := json.Marshal(record)
|
||||
recordVal := string(recordBytes)
|
||||
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
recStr, recErr := tx.Get(recordKey)
|
||||
if recErr == nil && recStr != "" {
|
||||
var existing PasswordResetRecord
|
||||
jErr := json.Unmarshal([]byte(recStr), &existing)
|
||||
cooldown := time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Cooldown)
|
||||
if jErr == nil && time.Since(existing.TimeCreated) < cooldown {
|
||||
err = errLimitExceeded
|
||||
return nil
|
||||
}
|
||||
}
|
||||
tx.Set(recordKey, recordVal, &buntdb.SetOptions{
|
||||
Expires: true,
|
||||
TTL: time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Timeout),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf(client.t("Reset your password on %s"), am.server.name)
|
||||
message := email.ComposeMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, subject)
|
||||
fmt.Fprintf(&message, client.t("We received a request to reset your password on %[1]s for account: %[2]s"), am.server.name, account.Name)
|
||||
message.WriteString("\r\n")
|
||||
message.WriteString(client.t("If you did not initiate this request, you can safely ignore this message."))
|
||||
message.WriteString("\r\n")
|
||||
message.WriteString("\r\n")
|
||||
message.WriteString(client.t("Otherwise, to reset your password, issue the following command (replace `new_password` with your desired password):"))
|
||||
message.WriteString("\r\n")
|
||||
fmt.Fprintf(&message, "/MSG NickServ RESETPASS %s %s new_password\r\n", account.Name, record.Code)
|
||||
|
||||
err = email.SendMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, message.Bytes())
|
||||
if err == nil {
|
||||
am.server.logger.Info("services",
|
||||
fmt.Sprintf("client %s sent a password reset email for account %s", client.Nick(), account.Name))
|
||||
} else {
|
||||
am.server.logger.Error("internal", "Failed to dispatch e-mail to", account.Settings.Email, err.Error())
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func (am *AccountManager) NsResetpass(client *Client, accountName, code, password string) (err error) {
|
||||
if ValidatePassphrase(password) != nil {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
account, err := am.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !account.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
if account.Suspended != nil {
|
||||
return errAccountSuspended
|
||||
}
|
||||
|
||||
success := false
|
||||
key := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
rawStr, err := tx.Get(key)
|
||||
if err == nil && rawStr != "" {
|
||||
var record PasswordResetRecord
|
||||
err := json.Unmarshal([]byte(rawStr), &record)
|
||||
if err == nil && utils.SecretTokensMatch(record.Code, code) {
|
||||
success = true
|
||||
tx.Delete(key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if success {
|
||||
return am.setPassword(accountName, password, true)
|
||||
} else {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
}
|
||||
|
||||
type PasswordResetRecord struct {
|
||||
TimeCreated time.Time
|
||||
Code string
|
||||
}
|
||||
|
||||
func marshalReservedNicks(nicks []string) string {
|
||||
return strings.Join(nicks, ",")
|
||||
}
|
||||
@ -1080,6 +1411,11 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
|
||||
if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
|
||||
err = errAccountInvalidCredentials
|
||||
}
|
||||
if err == nil && account.Credentials.SCRAMCreds.Iters == 0 {
|
||||
// XXX: if the account was created prior to 2.8, it doesn't have SCRAM credentials;
|
||||
// since we temporarily have access to a valid plaintext password, create them:
|
||||
am.rehashPassword(account.Name, passphrase)
|
||||
}
|
||||
case -1:
|
||||
err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
|
||||
case -2:
|
||||
@ -1099,13 +1435,17 @@ func (am *AccountManager) checkLegacyPassphrase(check migrations.PassphraseCheck
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
// re-hash the passphrase with the latest algorithm
|
||||
err = am.setPassword(account, passphrase, true)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "could not upgrade user password", err.Error())
|
||||
}
|
||||
am.rehashPassword(account, passphrase)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) rehashPassword(accountName, passphrase string) {
|
||||
err := am.setPassword(accountName, passphrase, true)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "could not upgrade user password", accountName, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) {
|
||||
account, err = am.LoadAccount(accountName)
|
||||
if err == errAccountDoesNotExist && autocreate {
|
||||
@ -1161,6 +1501,74 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByBearerToken(client *Client, tokenType, token string) (err error) {
|
||||
switch tokenType {
|
||||
case "oauth2":
|
||||
return am.AuthenticateByOAuthBearer(client, oauth2.OAuthBearerOptions{Token: token})
|
||||
case "jwt":
|
||||
return am.AuthenticateByJWT(client, token)
|
||||
default:
|
||||
return errInvalidBearerTokenType
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByOAuthBearer(client *Client, opts oauth2.OAuthBearerOptions) (err error) {
|
||||
config := am.server.Config()
|
||||
|
||||
if !config.Accounts.OAuth2.Enabled {
|
||||
return errFeatureDisabled
|
||||
}
|
||||
|
||||
if throttled, remainingTime := client.checkLoginThrottle(); throttled {
|
||||
return &ThrottleError{remainingTime}
|
||||
}
|
||||
|
||||
var username string
|
||||
if config.Accounts.AuthScript.Enabled && config.Accounts.OAuth2.AuthScript {
|
||||
username, err = am.authenticateByOAuthBearerScript(client, config, opts)
|
||||
} else {
|
||||
username, err = config.Accounts.OAuth2.Introspect(context.Background(), opts.Token)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account, err := am.loadWithAutocreation(username, config.Accounts.OAuth2.Autocreate)
|
||||
if err == nil {
|
||||
am.Login(client, account)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByJWT(client *Client, token string) (err error) {
|
||||
config := am.server.Config()
|
||||
// enabled check is encapsulated here:
|
||||
accountName, err := config.Accounts.JWTAuth.Validate(token)
|
||||
if err != nil {
|
||||
am.server.logger.Debug("accounts", "invalid JWT token", err.Error())
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
account, err := am.loadWithAutocreation(accountName, config.Accounts.JWTAuth.Autocreate)
|
||||
if err == nil {
|
||||
am.Login(client, account)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *AccountManager) authenticateByOAuthBearerScript(client *Client, config *Config, opts oauth2.OAuthBearerOptions) (username string, err error) {
|
||||
output, err := CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
||||
AuthScriptInput{OAuthBearer: &opts, IP: client.IP().String()})
|
||||
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
return "", oauth2.ErrInvalidToken
|
||||
} else if output.Success {
|
||||
return output.AccountName, nil
|
||||
} else {
|
||||
return "", oauth2.ErrInvalidToken
|
||||
}
|
||||
}
|
||||
|
||||
// AllNicks returns the uncasefolded nicknames for all accounts, including additional (grouped) nicks.
|
||||
func (am *AccountManager) AllNicks() (result []string) {
|
||||
accountNamePrefix := fmt.Sprintf(keyAccountName, "")
|
||||
@ -1216,6 +1624,22 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) accountWasUnregistered(accountName string) (result bool) {
|
||||
casefoldedAccount, err := CasefoldName(accountName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
if _, err := tx.Get(unregisteredKey); err == nil {
|
||||
result = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// look up the unfolded version of an account name, possibly after deletion
|
||||
func (am *AccountManager) AccountToAccountName(account string) (result string) {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
@ -1284,7 +1708,6 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
@ -1299,7 +1722,6 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
result.Name, _ = tx.Get(accountNameKey)
|
||||
result.RegisteredAt, _ = tx.Get(registeredTimeKey)
|
||||
result.Credentials, _ = tx.Get(credentialsKey)
|
||||
result.Callback, _ = tx.Get(callbackKey)
|
||||
result.AdditionalNicks, _ = tx.Get(nicksKey)
|
||||
result.VHost, _ = tx.Get(vhostKey)
|
||||
result.Settings, _ = tx.Get(settingsKey)
|
||||
@ -1479,29 +1901,31 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
||||
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
||||
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
|
||||
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
|
||||
suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
|
||||
pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
|
||||
emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
||||
pushSubscriptionsKey := fmt.Sprintf(keyAccountPushSubscriptions, casefoldedAccount)
|
||||
metadataKey := fmt.Sprintf(keyAccountMetadata, casefoldedAccount)
|
||||
|
||||
var clients []*Client
|
||||
defer func() {
|
||||
am.killClients(clients)
|
||||
}()
|
||||
|
||||
var registeredChannels []string
|
||||
// on our way out, unregister all the account's channels and delete them from the db
|
||||
defer func() {
|
||||
for _, channelName := range registeredChannels {
|
||||
for _, channelName := range am.server.channels.ChannelsForAccount(casefoldedAccount) {
|
||||
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
|
||||
@ -1516,7 +1940,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
defer am.serialCacheUpdateMutex.Unlock()
|
||||
|
||||
var accountName string
|
||||
var channelsStr string
|
||||
keepProtections := false
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// get the unfolded account name; for an active account, this is
|
||||
@ -1537,7 +1960,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
tx.Delete(accountNameKey)
|
||||
tx.Delete(verifiedKey)
|
||||
tx.Delete(registeredTimeKey)
|
||||
tx.Delete(callbackKey)
|
||||
tx.Delete(verificationCodeKey)
|
||||
tx.Delete(settingsKey)
|
||||
rawNicks, _ = tx.Get(nicksKey)
|
||||
@ -1545,13 +1967,16 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
credText, err = tx.Get(credentialsKey)
|
||||
tx.Delete(credentialsKey)
|
||||
tx.Delete(vhostKey)
|
||||
channelsStr, _ = tx.Get(channelsKey)
|
||||
tx.Delete(channelsKey)
|
||||
tx.Delete(joinedChannelsKey)
|
||||
tx.Delete(lastSeenKey)
|
||||
tx.Delete(readMarkersKey)
|
||||
tx.Delete(modesKey)
|
||||
tx.Delete(realnameKey)
|
||||
tx.Delete(suspendedKey)
|
||||
tx.Delete(pwResetKey)
|
||||
tx.Delete(emailChangeKey)
|
||||
tx.Delete(pushSubscriptionsKey)
|
||||
tx.Delete(metadataKey)
|
||||
|
||||
return nil
|
||||
})
|
||||
@ -1573,7 +1998,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
|
||||
skeleton, _ := Skeleton(accountName)
|
||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||
registeredChannels = unmarshalRegisteredChannels(channelsStr)
|
||||
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
@ -1605,21 +2029,6 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) ChannelsForAccount(account string) (channels []string) {
|
||||
cfaccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var channelStr string
|
||||
key := fmt.Sprintf(keyAccountChannels, cfaccount)
|
||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
channelStr, _ = tx.Get(key)
|
||||
return nil
|
||||
})
|
||||
return unmarshalRegisteredChannels(channelStr)
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
||||
if certfp == "" {
|
||||
return errAccountInvalidCredentials
|
||||
@ -1676,8 +2085,10 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin
|
||||
return err
|
||||
}
|
||||
|
||||
if authzid != "" && authzid != account {
|
||||
return errAuthzidAuthcidMismatch
|
||||
if authzid != "" {
|
||||
if cfAuthzid, err := CasefoldName(authzid); err != nil || cfAuthzid != account {
|
||||
return errAuthzidAuthcidMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
@ -1878,9 +2289,12 @@ func (am *AccountManager) Logout(client *Client) {
|
||||
var (
|
||||
// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
|
||||
// This can be moved to some other data structure/place if we need to load/unload mechs later.
|
||||
EnabledSaslMechanisms = map[string]func(*Server, *Client, string, []byte, *ResponseBuffer) bool{
|
||||
"PLAIN": authPlainHandler,
|
||||
"EXTERNAL": authExternalHandler,
|
||||
EnabledSaslMechanisms = map[string]func(*Server, *Client, *Session, []byte, *ResponseBuffer) bool{
|
||||
"PLAIN": authPlainHandler,
|
||||
"EXTERNAL": authExternalHandler,
|
||||
"SCRAM-SHA-256": authScramHandler,
|
||||
"OAUTHBEARER": authOauthBearerHandler,
|
||||
"IRCV3BEARER": authIRCv3BearerHandler,
|
||||
}
|
||||
)
|
||||
|
||||
@ -1894,11 +2308,19 @@ const (
|
||||
CredentialsAnope = -2
|
||||
)
|
||||
|
||||
type SCRAMCreds struct {
|
||||
Salt []byte
|
||||
Iters int
|
||||
StoredKey []byte
|
||||
ServerKey []byte
|
||||
}
|
||||
|
||||
// AccountCredentials stores the various methods for verifying accounts.
|
||||
type AccountCredentials struct {
|
||||
Version CredentialsVersion
|
||||
PassphraseHash []byte
|
||||
Certfps []string
|
||||
SCRAMCreds
|
||||
}
|
||||
|
||||
func (ac *AccountCredentials) Empty() bool {
|
||||
@ -1918,10 +2340,11 @@ func (ac *AccountCredentials) Serialize() (result string, err error) {
|
||||
func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
|
||||
if passphrase == "" {
|
||||
ac.PassphraseHash = nil
|
||||
ac.SCRAMCreds = SCRAMCreds{}
|
||||
return nil
|
||||
}
|
||||
|
||||
if validatePassphrase(passphrase) != nil {
|
||||
if ValidatePassphrase(passphrase) != nil {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
|
||||
@ -1930,9 +2353,54 @@ func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint)
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
|
||||
// we can pass an empty account name because it won't actually be incorporated
|
||||
// into the credentials; it's just a quirk of the xdg-go/scram API that the way
|
||||
// to produce server credentials is to call NewClient* and then GetStoredCredentials
|
||||
scramClient, err := scram.SHA256.NewClientUnprepped("", passphrase, "")
|
||||
if err != nil {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
salt := make([]byte, 16)
|
||||
rand.Read(salt)
|
||||
// xdg-go/scram says: "Clients have a default minimum PBKDF2 iteration count of 4096."
|
||||
minIters := 4096
|
||||
scramCreds := scramClient.GetStoredCredentials(scram.KeyFactors{Salt: string(salt), Iters: minIters})
|
||||
ac.SCRAMCreds = SCRAMCreds{
|
||||
Salt: salt,
|
||||
Iters: minIters,
|
||||
StoredKey: scramCreds.StoredKey,
|
||||
ServerKey: scramCreds.ServerKey,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) NewScramConversation() *scram.ServerConversation {
|
||||
server, _ := scram.SHA256.NewServer(am.lookupSCRAMCreds)
|
||||
return server.NewConversation()
|
||||
}
|
||||
|
||||
func (am *AccountManager) lookupSCRAMCreds(accountName string) (creds scram.StoredCredentials, err error) {
|
||||
// strip client ID if present:
|
||||
if strudelIndex := strings.IndexByte(accountName, '@'); strudelIndex != -1 {
|
||||
accountName = accountName[:strudelIndex]
|
||||
}
|
||||
|
||||
acct, err := am.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if acct.Credentials.SCRAMCreds.Iters == 0 {
|
||||
err = errNoSCRAMCredentials
|
||||
return
|
||||
}
|
||||
creds.Salt = string(acct.Credentials.SCRAMCreds.Salt)
|
||||
creds.Iters = acct.Credentials.SCRAMCreds.Iters
|
||||
creds.StoredKey = acct.Credentials.SCRAMCreds.StoredKey
|
||||
creds.ServerKey = acct.Credentials.SCRAMCreds.ServerKey
|
||||
return
|
||||
}
|
||||
|
||||
func (ac *AccountCredentials) AddCertfp(certfp string) (err error) {
|
||||
// XXX we require that certfp is already normalized (rather than normalize here
|
||||
// and pass back the normalized version as an additional return parameter);
|
||||
@ -1989,7 +2457,6 @@ type ReplayJoinsSetting uint
|
||||
const (
|
||||
ReplayJoinsCommandsOnly = iota // replay in HISTORY or CHATHISTORY output
|
||||
ReplayJoinsAlways // replay in HISTORY, CHATHISTORY, or autoreplay
|
||||
ReplayJoinsNever // never replay
|
||||
)
|
||||
|
||||
func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err error) {
|
||||
@ -1998,8 +2465,6 @@ func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err er
|
||||
result = ReplayJoinsCommandsOnly
|
||||
case "always":
|
||||
result = ReplayJoinsAlways
|
||||
case "never":
|
||||
result = ReplayJoinsNever
|
||||
default:
|
||||
err = errInvalidParams
|
||||
}
|
||||
@ -2017,6 +2482,7 @@ type AccountSettings struct {
|
||||
AutoreplayMissed bool
|
||||
DMHistory HistoryStatus
|
||||
AutoAway PersistentStatus
|
||||
Email string
|
||||
}
|
||||
|
||||
// ClientAccount represents a user account.
|
||||
@ -2038,7 +2504,6 @@ type rawClientAccount struct {
|
||||
Name string
|
||||
RegisteredAt string
|
||||
Credentials string
|
||||
Callback string
|
||||
Verified bool
|
||||
AdditionalNicks string
|
||||
VHost string
|
||||
|
||||
311
irc/api.go
Normal file
311
irc/api.go
Normal file
@ -0,0 +1,311 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
func newAPIHandler(server *Server) http.Handler {
|
||||
api := &ergoAPI{
|
||||
server: server,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
api.mux.HandleFunc("POST /v1/rehash", api.handleRehash)
|
||||
api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)
|
||||
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
|
||||
api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)
|
||||
api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList)
|
||||
api.mux.HandleFunc("POST /v1/status", api.handleStatus)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
type ergoAPI struct {
|
||||
server *Server
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer a.server.HandlePanic(nil)
|
||||
defer a.server.logger.Debug("api", r.URL.Path)
|
||||
|
||||
if a.checkBearerAuth(r.Header.Get("Authorization")) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ergoAPI) checkBearerAuth(authHeader string) (authorized bool) {
|
||||
if authHeader == "" {
|
||||
return false
|
||||
}
|
||||
c := a.server.Config()
|
||||
if !c.API.Enabled {
|
||||
return false
|
||||
}
|
||||
spaceIdx := strings.IndexByte(authHeader, ' ')
|
||||
if spaceIdx < 0 {
|
||||
return false
|
||||
}
|
||||
if !strings.EqualFold("Bearer", authHeader[:spaceIdx]) {
|
||||
return false
|
||||
}
|
||||
providedTokenBytes := []byte(authHeader[spaceIdx+1:])
|
||||
for _, tokenBytes := range c.API.bearerTokenBytes {
|
||||
if subtle.ConstantTimeCompare(tokenBytes, providedTokenBytes) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *ergoAPI) decodeJSONRequest(request any, w http.ResponseWriter, r *http.Request) (err error) {
|
||||
err = json.NewDecoder(r.Body).Decode(request)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to deserialize json request: %v", err), http.StatusBadRequest)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *ergoAPI) writeJSONResponse(response any, w http.ResponseWriter, r *http.Request) {
|
||||
j, err := json.Marshal(response)
|
||||
if err == nil {
|
||||
j = append(j, '\n') // less annoying in curl output
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(j)
|
||||
} else {
|
||||
a.server.logger.Error("internal", "failed to serialize API response", r.URL.Path, err.Error())
|
||||
http.Error(w, fmt.Sprintf("failed to serialize json response: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type apiGenericResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
}
|
||||
|
||||
func (a *ergoAPI) handleRehash(w http.ResponseWriter, r *http.Request) {
|
||||
var response apiGenericResponse
|
||||
err := a.server.rehash()
|
||||
if err == nil {
|
||||
response.Success = true
|
||||
} else {
|
||||
response.Success = false
|
||||
response.Error = err.Error()
|
||||
}
|
||||
a.writeJSONResponse(response, w, r)
|
||||
}
|
||||
|
||||
type apiCheckAuthResponse struct {
|
||||
apiGenericResponse
|
||||
AccountName string `json:"accountName,omitempty"`
|
||||
}
|
||||
|
||||
func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) {
|
||||
var request AuthScriptInput
|
||||
if err := a.decodeJSONRequest(&request, w, r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response apiCheckAuthResponse
|
||||
|
||||
// try passphrase if present
|
||||
if request.AccountName != "" && request.Passphrase != "" {
|
||||
account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
|
||||
switch err {
|
||||
case nil:
|
||||
// success, no error
|
||||
response.Success = true
|
||||
response.AccountName = account.Name
|
||||
case errAccountDoesNotExist, errAccountInvalidCredentials, errAccountUnverified, errAccountSuspended:
|
||||
// fail, no error
|
||||
response.Success = false
|
||||
default:
|
||||
response.Success = false
|
||||
response.Error = err.Error()
|
||||
}
|
||||
}
|
||||
// try certfp if present
|
||||
if !response.Success && request.Certfp != "" {
|
||||
// TODO support cerftp
|
||||
}
|
||||
|
||||
a.writeJSONResponse(response, w, r)
|
||||
}
|
||||
|
||||
type apiSaregisterRequest struct {
|
||||
AccountName string `json:"accountName"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
}
|
||||
|
||||
func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) {
|
||||
var request apiSaregisterRequest
|
||||
if err := a.decodeJSONRequest(&request, w, r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response apiGenericResponse
|
||||
err := a.server.accounts.SARegister(request.AccountName, request.Passphrase)
|
||||
if err == nil {
|
||||
response.Success = true
|
||||
} else {
|
||||
response.Success = false
|
||||
response.Error = err.Error()
|
||||
switch err {
|
||||
case errAccountAlreadyRegistered, errAccountAlreadyVerified, errNameReserved:
|
||||
response.ErrorCode = "ACCOUNT_EXISTS"
|
||||
case errAccountBadPassphrase:
|
||||
response.ErrorCode = "INVALID_PASSPHRASE"
|
||||
default:
|
||||
response.ErrorCode = "UNKNOWN_ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
a.writeJSONResponse(response, w, r)
|
||||
}
|
||||
|
||||
type apiAccountDetailsResponse struct {
|
||||
apiGenericResponse
|
||||
AccountName string `json:"accountName,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
RegisteredAt string `json:"registeredAt,omitempty"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
}
|
||||
|
||||
type apiAccountDetailsRequest struct {
|
||||
AccountName string `json:"accountName"`
|
||||
}
|
||||
|
||||
func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
||||
var request apiAccountDetailsRequest
|
||||
if err := a.decodeJSONRequest(&request, w, r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response apiAccountDetailsResponse
|
||||
|
||||
if request.AccountName != "" {
|
||||
accountData, err := a.server.accounts.LoadAccount(request.AccountName)
|
||||
if err == nil {
|
||||
if !accountData.Verified {
|
||||
err = errAccountUnverified
|
||||
} else if accountData.Suspended != nil {
|
||||
err = errAccountSuspended
|
||||
}
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
response.AccountName = accountData.Name
|
||||
response.Email = accountData.Settings.Email
|
||||
if !accountData.RegisteredAt.IsZero() {
|
||||
response.RegisteredAt = accountData.RegisteredAt.Format(utils.IRCv3TimestampFormat)
|
||||
}
|
||||
|
||||
// Get channels the account is in
|
||||
response.Channels = a.server.channels.ChannelsForAccount(accountData.NameCasefolded)
|
||||
response.Success = true
|
||||
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
|
||||
response.Success = false
|
||||
default:
|
||||
response.Success = false
|
||||
response.ErrorCode = "UNKNOWN_ERROR"
|
||||
response.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
response.Success = false
|
||||
response.ErrorCode = "INVALID_REQUEST"
|
||||
}
|
||||
|
||||
a.writeJSONResponse(response, w, r)
|
||||
}
|
||||
|
||||
type apiAccountListResponse struct {
|
||||
apiGenericResponse
|
||||
Accounts []apiAccountDetailsResponse `json:"accounts"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
|
||||
func (a *ergoAPI) handleAccountList(w http.ResponseWriter, r *http.Request) {
|
||||
var response apiAccountListResponse
|
||||
|
||||
// Get all account names
|
||||
accounts := a.server.accounts.AllNicks()
|
||||
response.TotalCount = len(accounts)
|
||||
|
||||
// Load account details
|
||||
response.Accounts = make([]apiAccountDetailsResponse, len(accounts))
|
||||
for i, account := range accounts {
|
||||
accountData, err := a.server.accounts.LoadAccount(account)
|
||||
if err != nil {
|
||||
response.Accounts[i] = apiAccountDetailsResponse{
|
||||
apiGenericResponse: apiGenericResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
},
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
response.Accounts[i] = apiAccountDetailsResponse{
|
||||
apiGenericResponse: apiGenericResponse{
|
||||
Success: true,
|
||||
},
|
||||
AccountName: accountData.Name,
|
||||
Email: accountData.Settings.Email,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success = true
|
||||
a.writeJSONResponse(response, w, r)
|
||||
}
|
||||
|
||||
type apiStatusResponse struct {
|
||||
apiGenericResponse
|
||||
Version string `json:"version"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
StartTime string `json:"start_time"`
|
||||
Users struct {
|
||||
Total int `json:"total"`
|
||||
Invisible int `json:"invisible"`
|
||||
Operators int `json:"operators"`
|
||||
Unknown int `json:"unknown"`
|
||||
Max int `json:"max"`
|
||||
} `json:"users"`
|
||||
Channels int `json:"channels"`
|
||||
Servers int `json:"servers"`
|
||||
}
|
||||
|
||||
func (a *ergoAPI) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
server := a.server
|
||||
stats := server.stats.GetValues()
|
||||
|
||||
response := apiStatusResponse{
|
||||
apiGenericResponse: apiGenericResponse{Success: true},
|
||||
Version: SemVer,
|
||||
GoVersion: runtime.Version(),
|
||||
Commit: Commit,
|
||||
StartTime: server.ctime.Format(utils.IRCv3TimestampFormat),
|
||||
}
|
||||
|
||||
response.Users.Total = stats.Total
|
||||
response.Users.Invisible = stats.Invisible
|
||||
response.Users.Operators = stats.Operators
|
||||
response.Users.Unknown = stats.Unknown
|
||||
response.Users.Max = stats.Max
|
||||
response.Channels = server.channels.Len()
|
||||
response.Servers = 1
|
||||
|
||||
a.writeJSONResponse(response, w, r)
|
||||
}
|
||||
@ -10,7 +10,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/oauth2"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// JSON-serializable input and output types for the script
|
||||
@ -20,7 +21,8 @@ type AuthScriptInput struct {
|
||||
Certfp string `json:"certfp,omitempty"`
|
||||
PeerCerts []string `json:"peerCerts,omitempty"`
|
||||
peerCerts []*x509.Certificate
|
||||
IP string `json:"ip,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
OAuthBearer *oauth2.OAuthBearerOptions `json:"oauth2,omitempty"`
|
||||
}
|
||||
|
||||
type AuthScriptOutput struct {
|
||||
@ -84,7 +86,7 @@ type IPScriptOutput struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func CheckIPBan(sem utils.Semaphore, config ScriptConfig, addr net.IP) (output IPScriptOutput, err error) {
|
||||
func CheckIPBan(sem utils.Semaphore, config IPCheckScriptConfig, addr net.IP) (output IPScriptOutput, err error) {
|
||||
if sem != nil {
|
||||
sem.Acquire()
|
||||
defer sem.Release()
|
||||
|
||||
107
irc/bunt/bunt_datastore.go
Normal file
107
irc/bunt/bunt_datastore.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2022 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package bunt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/logger"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// BuntKey yields a string key corresponding to a (table, UUID) pair.
|
||||
// Ideally this would not be public, but some of the migration code
|
||||
// needs it.
|
||||
func BuntKey(table datastore.Table, uuid utils.UUID) string {
|
||||
return fmt.Sprintf("%x %s", table, uuid.String())
|
||||
}
|
||||
|
||||
// buntdbDatastore implements datastore.Datastore using a buntdb.
|
||||
type buntdbDatastore struct {
|
||||
db *buntdb.DB
|
||||
logger *logger.Manager
|
||||
}
|
||||
|
||||
// NewBuntdbDatastore returns a datastore.Datastore backed by buntdb.
|
||||
func NewBuntdbDatastore(db *buntdb.DB, logger *logger.Manager) datastore.Datastore {
|
||||
return &buntdbDatastore{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *buntdbDatastore) Backoff() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *buntdbDatastore) GetAll(table datastore.Table) (result []datastore.KV, err error) {
|
||||
tablePrefix := fmt.Sprintf("%x ", table)
|
||||
err = b.db.View(func(tx *buntdb.Tx) error {
|
||||
err := tx.AscendGreaterOrEqual("", tablePrefix, func(key, value string) bool {
|
||||
encUUID, ok := strings.CutPrefix(key, tablePrefix)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
uuid, err := utils.DecodeUUID(encUUID)
|
||||
if err == nil {
|
||||
result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)})
|
||||
} else {
|
||||
b.logger.Error("datastore", "invalid uuid", key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *buntdbDatastore) Get(table datastore.Table, uuid utils.UUID) (value []byte, err error) {
|
||||
buntKey := BuntKey(table, uuid)
|
||||
var result string
|
||||
err = b.db.View(func(tx *buntdb.Tx) error {
|
||||
result, err = tx.Get(buntKey)
|
||||
return err
|
||||
})
|
||||
return []byte(result), err
|
||||
}
|
||||
|
||||
func (b *buntdbDatastore) Set(table datastore.Table, uuid utils.UUID, value []byte, expiration time.Time) (err error) {
|
||||
buntKey := BuntKey(table, uuid)
|
||||
var setOptions *buntdb.SetOptions
|
||||
if !expiration.IsZero() {
|
||||
ttl := time.Until(expiration)
|
||||
if ttl > 0 {
|
||||
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
|
||||
} else {
|
||||
return nil // it already expired, i guess?
|
||||
}
|
||||
}
|
||||
strVal := string(value)
|
||||
|
||||
err = b.db.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err := tx.Set(buntKey, strVal, setOptions)
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *buntdbDatastore) Delete(table datastore.Table, key utils.UUID) (err error) {
|
||||
buntKey := BuntKey(table, key)
|
||||
err = b.db.Update(func(tx *buntdb.Tx) error {
|
||||
_, err := tx.Delete(buntKey)
|
||||
return err
|
||||
})
|
||||
// deleting a nonexistent key is not considered an error
|
||||
switch err {
|
||||
case buntdb.ErrNotFound:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -60,10 +60,15 @@ const (
|
||||
MultilineConcatTag = "draft/multiline-concat"
|
||||
// draft/relaymsg:
|
||||
RelaymsgTagName = "draft/relaymsg"
|
||||
// BOT mode: https://ircv3.net/specs/extensions/bot-mode
|
||||
BotTagName = "bot"
|
||||
// https://ircv3.net/specs/extensions/chathistory
|
||||
ChathistoryTargetsBatchType = "draft/chathistory-targets"
|
||||
ExtendedISupportBatchType = "draft/isupport"
|
||||
)
|
||||
|
||||
func init() {
|
||||
nameToCapability = make(map[string]Capability)
|
||||
nameToCapability = make(map[string]Capability, numCapabs)
|
||||
for capab, name := range capabilityNames {
|
||||
nameToCapability[name] = Capability(capab)
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@ package caps
|
||||
|
||||
const (
|
||||
// number of recognized capabilities:
|
||||
numCapabs = 28
|
||||
// length of the uint64 array that represents the bitset:
|
||||
bitsetLen = 1
|
||||
numCapabs = 38
|
||||
// length of the uint32 array that represents the bitset:
|
||||
bitsetLen = 2
|
||||
)
|
||||
|
||||
const (
|
||||
@ -37,6 +37,10 @@ const (
|
||||
// https://ircv3.net/specs/extensions/chghost-3.2.html
|
||||
ChgHost Capability = iota
|
||||
|
||||
// AccountRegistration is the draft IRCv3 capability named "draft/account-registration":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/435
|
||||
AccountRegistration Capability = iota
|
||||
|
||||
// ChannelRename is the draft IRCv3 capability named "draft/channel-rename":
|
||||
// https://ircv3.net/specs/extensions/channel-rename
|
||||
ChannelRename Capability = iota
|
||||
@ -49,34 +53,66 @@ const (
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/362
|
||||
EventPlayback Capability = iota
|
||||
|
||||
// ExtendedISupport is the proposed IRCv3 capability named "draft/extended-isupport":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/543
|
||||
ExtendedISupport Capability = iota
|
||||
|
||||
// Languages is the proposed IRCv3 capability named "draft/languages":
|
||||
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
||||
Languages Capability = iota
|
||||
|
||||
// MessageRedaction is the proposed IRCv3 capability named "draft/message-redaction":
|
||||
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
|
||||
MessageRedaction Capability = iota
|
||||
|
||||
// Metadata is the draft IRCv3 capability named "draft/metadata-2":
|
||||
// https://ircv3.net/specs/extensions/metadata
|
||||
Metadata Capability = iota
|
||||
|
||||
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||
Multiline Capability = iota
|
||||
|
||||
// Register is the proposed IRCv3 capability named "draft/register":
|
||||
// https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495
|
||||
Register Capability = iota
|
||||
// NoImplicitNames is the proposed IRCv3 capability named "draft/no-implicit-names":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/527
|
||||
NoImplicitNames Capability = iota
|
||||
|
||||
// Persistence is the proposed IRCv3 capability named "draft/persistence":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/503
|
||||
Persistence Capability = iota
|
||||
|
||||
// Preaway is the proposed IRCv3 capability named "draft/pre-away":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/514
|
||||
Preaway Capability = iota
|
||||
|
||||
// ReadMarker is the draft IRCv3 capability named "draft/read-marker":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/489
|
||||
ReadMarker Capability = iota
|
||||
|
||||
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/417
|
||||
Relaymsg Capability = iota
|
||||
|
||||
// Resume is the proposed IRCv3 capability named "draft/resume-0.5":
|
||||
// https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
|
||||
Resume Capability = iota
|
||||
// WebPush is the proposed IRCv3 capability named "draft/webpush":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/471
|
||||
WebPush Capability = iota
|
||||
|
||||
// EchoMessage is the IRCv3 capability named "echo-message":
|
||||
// https://ircv3.net/specs/extensions/echo-message-3.2.html
|
||||
EchoMessage Capability = iota
|
||||
|
||||
// Nope is the Ergo vendor capability named "ergo.chat/nope":
|
||||
// https://ergo.chat/nope
|
||||
Nope Capability = iota
|
||||
|
||||
// ExtendedJoin is the IRCv3 capability named "extended-join":
|
||||
// https://ircv3.net/specs/extensions/extended-join-3.1.html
|
||||
ExtendedJoin Capability = iota
|
||||
|
||||
// ExtendedMonitor is the IRCv3 capability named "extended-monitor":
|
||||
// https://ircv3.net/specs/extensions/extended-monitor.html
|
||||
ExtendedMonitor Capability = iota
|
||||
|
||||
// InviteNotify is the IRCv3 capability named "invite-notify":
|
||||
// https://ircv3.net/specs/extensions/invite-notify-3.2.html
|
||||
InviteNotify Capability = iota
|
||||
@ -93,10 +129,6 @@ const (
|
||||
// https://ircv3.net/specs/extensions/multi-prefix-3.1.html
|
||||
MultiPrefix Capability = iota
|
||||
|
||||
// Nope is the Oragono vendor capability named "oragono.io/nope":
|
||||
// https://oragono.io/nope
|
||||
Nope Capability = iota
|
||||
|
||||
// SASL is the IRCv3 capability named "sasl":
|
||||
// https://ircv3.net/specs/extensions/sasl-3.2.html
|
||||
SASL Capability = iota
|
||||
@ -109,6 +141,14 @@ const (
|
||||
// https://ircv3.net/specs/extensions/setname.html
|
||||
SetName Capability = iota
|
||||
|
||||
// SojuWebPush is the Soju/Goguma vendor capability named "soju.im/webpush":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/471
|
||||
SojuWebPush Capability = iota
|
||||
|
||||
// StandardReplies is the IRCv3 capability named "standard-replies":
|
||||
// https://github.com/ircv3/ircv3-specifications/pull/506
|
||||
StandardReplies Capability = iota
|
||||
|
||||
// STS is the IRCv3 capability named "sts":
|
||||
// https://ircv3.net/specs/extensions/sts.html
|
||||
STS Capability = iota
|
||||
@ -135,24 +175,34 @@ var (
|
||||
"batch",
|
||||
"cap-notify",
|
||||
"chghost",
|
||||
"draft/account-registration",
|
||||
"draft/channel-rename",
|
||||
"draft/chathistory",
|
||||
"draft/event-playback",
|
||||
"draft/extended-isupport",
|
||||
"draft/languages",
|
||||
"draft/message-redaction",
|
||||
"draft/metadata-2",
|
||||
"draft/multiline",
|
||||
"draft/register",
|
||||
"draft/no-implicit-names",
|
||||
"draft/persistence",
|
||||
"draft/pre-away",
|
||||
"draft/read-marker",
|
||||
"draft/relaymsg",
|
||||
"draft/resume-0.5",
|
||||
"draft/webpush",
|
||||
"echo-message",
|
||||
"ergo.chat/nope",
|
||||
"extended-join",
|
||||
"extended-monitor",
|
||||
"invite-notify",
|
||||
"labeled-response",
|
||||
"message-tags",
|
||||
"multi-prefix",
|
||||
"oragono.io/nope",
|
||||
"sasl",
|
||||
"server-time",
|
||||
"setname",
|
||||
"soju.im/webpush",
|
||||
"standard-replies",
|
||||
"sts",
|
||||
"userhost-in-names",
|
||||
"znc.in/playback",
|
||||
|
||||
@ -5,7 +5,7 @@ package caps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// Set holds a set of enabled capabilities.
|
||||
@ -102,6 +102,13 @@ func (s *Set) Strings(version Version, values Values, maxLen int) (result []stri
|
||||
var capab Capability
|
||||
asSlice := s[:]
|
||||
for capab = 0; capab < numCapabs; capab++ {
|
||||
// XXX clients that only support CAP LS 301 cannot handle multiline
|
||||
// responses. omit some CAPs in this case, forcing the response to fit on
|
||||
// a single line. this is technically buggy for CAP LIST (as opposed to LS)
|
||||
// but it shouldn't matter
|
||||
if version < Cap302 && !isAllowed301(capab) {
|
||||
continue
|
||||
}
|
||||
// skip any capabilities that are not enabled
|
||||
if !utils.BitsetGet(asSlice, uint(capab)) {
|
||||
continue
|
||||
@ -122,3 +129,15 @@ func (s *Set) Strings(version Version, values Values, maxLen int) (result []stri
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// this is a fixed whitelist of caps that are eligible for display in CAP LS 301
|
||||
func isAllowed301(capab Capability) bool {
|
||||
switch capab {
|
||||
case AccountNotify, AccountTag, AwayNotify, Batch, ChgHost, Chathistory, EventPlayback,
|
||||
Relaymsg, EchoMessage, Nope, ExtendedJoin, InviteNotify, LabeledResponse, MessageTags,
|
||||
MultiPrefix, SASL, ServerTime, SetName, STS, UserhostInNames, ZNCSelfMessage, ZNCPlayback:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,11 @@
|
||||
|
||||
package caps
|
||||
|
||||
import "testing"
|
||||
import "reflect"
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSets(t *testing.T) {
|
||||
s1 := NewSet()
|
||||
@ -60,6 +63,19 @@ func TestSets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(found, expected interface{}) {
|
||||
if !reflect.DeepEqual(found, expected) {
|
||||
panic(fmt.Sprintf("found %#v, expected %#v", found, expected))
|
||||
}
|
||||
}
|
||||
|
||||
func Test301WhitelistNotRespectedFor302(t *testing.T) {
|
||||
s1 := NewSet()
|
||||
s1.Enable(AccountTag, EchoMessage, StandardReplies)
|
||||
assertEqual(s1.Strings(Cap301, nil, 0), []string{"account-tag echo-message"})
|
||||
assertEqual(s1.Strings(Cap302, nil, 0), []string{"account-tag echo-message standard-replies"})
|
||||
}
|
||||
|
||||
func TestSubtract(t *testing.T) {
|
||||
s1 := NewSet(AccountTag, EchoMessage, UserhostInNames, ServerTime)
|
||||
|
||||
|
||||
644
irc/channel.go
644
irc/channel.go
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,10 @@ package irc
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type channelManagerEntry struct {
|
||||
@ -25,60 +27,75 @@ type channelManagerEntry struct {
|
||||
type ChannelManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||
chans map[string]*channelManagerEntry
|
||||
chansSkeletons utils.StringSet // skeletons of *unregistered* chans
|
||||
registeredChannels utils.StringSet // casefolds of registered chans
|
||||
registeredSkeletons utils.StringSet // skeletons of registered chans
|
||||
purgedChannels utils.StringSet // casefolds of purged chans
|
||||
server *Server
|
||||
chans map[string]*channelManagerEntry
|
||||
chansSkeletons utils.HashSet[string]
|
||||
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
|
||||
server *Server
|
||||
}
|
||||
|
||||
// NewChannelManager returns a new ChannelManager.
|
||||
func (cm *ChannelManager) Initialize(server *Server) {
|
||||
func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) {
|
||||
cm.chans = make(map[string]*channelManagerEntry)
|
||||
cm.chansSkeletons = make(utils.StringSet)
|
||||
cm.chansSkeletons = make(utils.HashSet[string])
|
||||
cm.server = server
|
||||
|
||||
cm.loadRegisteredChannels(server.Config())
|
||||
// purging should work even if registration is disabled
|
||||
cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
|
||||
return cm.loadRegisteredChannels(config)
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) loadRegisteredChannels(config *Config) {
|
||||
if !config.Channels.Registration.Enabled {
|
||||
func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
|
||||
allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rawNames := cm.server.channelRegistry.AllChannels()
|
||||
registeredChannels := make(utils.StringSet, len(rawNames))
|
||||
registeredSkeletons := make(utils.StringSet, len(rawNames))
|
||||
for _, name := range rawNames {
|
||||
cfname, err := CasefoldChannel(name)
|
||||
if err == nil {
|
||||
registeredChannels.Add(cfname)
|
||||
}
|
||||
skeleton, err := Skeleton(name)
|
||||
if err == nil {
|
||||
registeredSkeletons.Add(skeleton)
|
||||
}
|
||||
}
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
cm.registeredChannels = registeredChannels
|
||||
cm.registeredSkeletons = registeredSkeletons
|
||||
|
||||
cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
|
||||
for _, purge := range allPurgeRecords {
|
||||
cm.purgedChannels[purge.NameCasefolded] = purge
|
||||
}
|
||||
|
||||
for _, regInfo := range allChannels {
|
||||
cfname, err := CasefoldChannel(regInfo.Name)
|
||||
if err != nil {
|
||||
cm.server.logger.Error("channels", "couldn't casefold registered channel, skipping", regInfo.Name, err.Error())
|
||||
continue
|
||||
} else {
|
||||
cm.server.logger.Debug("channels", "initializing registered channel", regInfo.Name)
|
||||
}
|
||||
skeleton, err := Skeleton(regInfo.Name)
|
||||
if err == nil {
|
||||
cm.chansSkeletons.Add(skeleton)
|
||||
}
|
||||
|
||||
if _, ok := cm.purgedChannels[cfname]; !ok {
|
||||
ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
|
||||
cm.chans[cfname] = &channelManagerEntry{
|
||||
channel: ch,
|
||||
pendingJoins: 0,
|
||||
skeleton: skeleton,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns an existing channel with name equivalent to `name`, or nil
|
||||
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
||||
name, err := CasefoldChannel(name)
|
||||
if err == nil {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
entry := cm.chans[name]
|
||||
// if the channel is still loading, pretend we don't have it
|
||||
if entry != nil && entry.channel.IsLoaded() {
|
||||
return entry.channel
|
||||
}
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
entry := cm.chans[name]
|
||||
if entry != nil {
|
||||
return entry.channel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -92,48 +109,43 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
||||
return errNoSuchChannel, ""
|
||||
}
|
||||
|
||||
channel, err := func() (*Channel, error) {
|
||||
channel, err, newChannel := func() (*Channel, error, bool) {
|
||||
var newChannel bool
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
|
||||
if cm.purgedChannels.Has(casefoldedName) {
|
||||
return nil, errChannelPurged
|
||||
// check purges first; a registered purged channel will still be present in `chans`
|
||||
if _, ok := cm.purgedChannels[casefoldedName]; ok {
|
||||
return nil, errChannelPurged, false
|
||||
}
|
||||
entry := cm.chans[casefoldedName]
|
||||
if entry == nil {
|
||||
registered := cm.registeredChannels.Has(casefoldedName)
|
||||
// enforce OpOnlyCreation
|
||||
if !registered && server.Config().Channels.OpOnlyCreation && !client.HasRoleCapabs("chanreg") {
|
||||
return nil, errInsufficientPrivs
|
||||
if server.Config().Channels.OpOnlyCreation &&
|
||||
!(isSajoin || client.HasRoleCapabs("chanreg")) {
|
||||
return nil, errInsufficientPrivs, false
|
||||
}
|
||||
// enforce confusables
|
||||
if !registered && (cm.chansSkeletons.Has(skeleton) || cm.registeredSkeletons.Has(skeleton)) {
|
||||
return nil, errConfusableIdentifier
|
||||
if cm.chansSkeletons.Has(skeleton) {
|
||||
return nil, errConfusableIdentifier, false
|
||||
}
|
||||
entry = &channelManagerEntry{
|
||||
channel: NewChannel(server, name, casefoldedName, registered),
|
||||
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
|
||||
pendingJoins: 0,
|
||||
}
|
||||
if !registered {
|
||||
// for an unregistered channel, we already have the correct unfolded name
|
||||
// and therefore the final skeleton. for a registered channel, we don't have
|
||||
// the unfolded name yet (it needs to be loaded from the db), but we already
|
||||
// have the final skeleton in `registeredSkeletons` so we don't need to track it
|
||||
cm.chansSkeletons.Add(skeleton)
|
||||
entry.skeleton = skeleton
|
||||
}
|
||||
cm.chansSkeletons.Add(skeleton)
|
||||
entry.skeleton = skeleton
|
||||
cm.chans[casefoldedName] = entry
|
||||
newChannel = true
|
||||
}
|
||||
entry.pendingJoins += 1
|
||||
return entry.channel, nil
|
||||
return entry.channel, nil, newChannel
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err, ""
|
||||
}
|
||||
|
||||
channel.EnsureLoaded()
|
||||
err, forward = channel.Join(client, key, isSajoin, rb)
|
||||
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
||||
|
||||
cm.maybeCleanup(channel, true)
|
||||
|
||||
@ -151,6 +163,10 @@ func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
|
||||
return
|
||||
}
|
||||
|
||||
cm.maybeCleanupInternal(cfname, entry, afterJoin)
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) maybeCleanupInternal(cfname string, entry *channelManagerEntry, afterJoin bool) {
|
||||
if afterJoin {
|
||||
entry.pendingJoins -= 1
|
||||
}
|
||||
@ -190,6 +206,10 @@ func (cm *ChannelManager) Cleanup(channel *Channel) {
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
|
||||
if account == "" {
|
||||
return errAuthRequired // this is already enforced by ChanServ, but do a final check
|
||||
}
|
||||
|
||||
if cm.server.Defcon() <= 4 {
|
||||
return errFeatureDisabled
|
||||
}
|
||||
@ -220,13 +240,6 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// transfer the skeleton from chansSkeletons to registeredSkeletons
|
||||
skeleton := entry.skeleton
|
||||
delete(cm.chansSkeletons, skeleton)
|
||||
entry.skeleton = ""
|
||||
cm.chans[cfname] = entry
|
||||
cm.registeredChannels.Add(cfname)
|
||||
cm.registeredSkeletons.Add(skeleton)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -236,17 +249,13 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := cm.server.channelRegistry.LoadChannel(cfname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Founder != account {
|
||||
return errChannelNotOwnedByAccount
|
||||
}
|
||||
var uuid utils.UUID
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = cm.server.channelRegistry.Delete(info)
|
||||
if delErr := cm.server.dstore.Delete(datastore.TableChannels, uuid); delErr != nil {
|
||||
cm.server.logger.Error("datastore", "couldn't delete channel registration", cfname, delErr.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@ -254,15 +263,14 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
||||
defer cm.Unlock()
|
||||
entry := cm.chans[cfname]
|
||||
if entry != nil {
|
||||
entry.channel.SetUnregistered(account)
|
||||
delete(cm.registeredChannels, cfname)
|
||||
// transfer the skeleton from registeredSkeletons to chansSkeletons
|
||||
if skel, err := Skeleton(entry.channel.Name()); err == nil {
|
||||
delete(cm.registeredSkeletons, skel)
|
||||
cm.chansSkeletons.Add(skel)
|
||||
entry.skeleton = skel
|
||||
cm.chans[cfname] = entry
|
||||
if entry.channel.Founder() != account {
|
||||
return errChannelNotOwnedByAccount
|
||||
}
|
||||
uuid = entry.channel.UUID()
|
||||
entry.channel.SetUnregistered(account) // changes the UUID
|
||||
// #1619: if the channel has 0 members and was only being retained
|
||||
// because it was registered, clean it up:
|
||||
cm.maybeCleanupInternal(cfname, entry, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -287,10 +295,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
var info RegisteredChannel
|
||||
defer func() {
|
||||
if channel != nil && info.Founder != "" {
|
||||
channel.Store(IncludeAllAttrs)
|
||||
// we just flushed the channel under its new name, therefore this delete
|
||||
// cannot be overwritten by a write to the old name:
|
||||
cm.server.channelRegistry.Delete(info)
|
||||
channel.MarkDirty(IncludeAllAttrs)
|
||||
}
|
||||
// always-on clients need to update their saved channel memberships
|
||||
for _, member := range channel.Members() {
|
||||
member.markDirty(IncludeChannels)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -298,11 +307,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
defer cm.Unlock()
|
||||
|
||||
entry := cm.chans[oldCfname]
|
||||
if entry == nil || !entry.channel.IsLoaded() {
|
||||
if entry == nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
channel = entry.channel
|
||||
info = channel.ExportRegistration(IncludeInitial)
|
||||
info = channel.ExportRegistration()
|
||||
registered := info.Founder != ""
|
||||
|
||||
oldSkeleton, err := Skeleton(info.Name)
|
||||
@ -311,13 +320,13 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
}
|
||||
|
||||
if newCfname != oldCfname {
|
||||
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
|
||||
if cm.chans[newCfname] != nil {
|
||||
return errChannelNameInUse
|
||||
}
|
||||
}
|
||||
|
||||
if oldSkeleton != newSkeleton {
|
||||
if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
|
||||
if cm.chansSkeletons.Has(newSkeleton) {
|
||||
return errConfusableIdentifier
|
||||
}
|
||||
}
|
||||
@ -327,15 +336,8 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
entry.skeleton = newSkeleton
|
||||
}
|
||||
cm.chans[newCfname] = entry
|
||||
if registered {
|
||||
delete(cm.registeredChannels, oldCfname)
|
||||
cm.registeredChannels.Add(newCfname)
|
||||
delete(cm.registeredSkeletons, oldSkeleton)
|
||||
cm.registeredSkeletons.Add(newSkeleton)
|
||||
} else {
|
||||
delete(cm.chansSkeletons, oldSkeleton)
|
||||
cm.chansSkeletons.Add(newSkeleton)
|
||||
}
|
||||
delete(cm.chansSkeletons, oldSkeleton)
|
||||
cm.chansSkeletons.Add(newSkeleton)
|
||||
entry.channel.Rename(newName, newCfname)
|
||||
return nil
|
||||
}
|
||||
@ -353,7 +355,18 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
|
||||
defer cm.RUnlock()
|
||||
result = make([]*Channel, 0, len(cm.chans))
|
||||
for _, entry := range cm.chans {
|
||||
if entry.channel.IsLoaded() {
|
||||
result = append(result, entry.channel)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListableChannels returns a slice of all non-purged channels.
|
||||
func (cm *ChannelManager) ListableChannels() (result []*Channel) {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
result = make([]*Channel, 0, len(cm.chans))
|
||||
for cfname, entry := range cm.chans {
|
||||
if _, ok := cm.purgedChannels[cfname]; !ok {
|
||||
result = append(result, entry.channel)
|
||||
}
|
||||
}
|
||||
@ -367,12 +380,45 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e
|
||||
return errInvalidChannelName
|
||||
}
|
||||
|
||||
cm.Lock()
|
||||
cm.purgedChannels.Add(chname)
|
||||
cm.Unlock()
|
||||
record.NameCasefolded = chname
|
||||
record.UUID = utils.GenerateUUIDv4()
|
||||
|
||||
cm.server.channelRegistry.PurgeChannel(chname, record)
|
||||
return nil
|
||||
channel, err := func() (channel *Channel, err error) {
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
|
||||
if _, ok := cm.purgedChannels[chname]; ok {
|
||||
return nil, errChannelPurgedAlready
|
||||
}
|
||||
|
||||
entry := cm.chans[chname]
|
||||
// atomically prevent anyone from rejoining
|
||||
cm.purgedChannels[chname] = record
|
||||
if entry != nil {
|
||||
channel = entry.channel
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel != nil {
|
||||
// actually kick everyone off the channel
|
||||
channel.Purge("")
|
||||
}
|
||||
|
||||
var purgeBytes []byte
|
||||
if purgeBytes, err = record.Serialize(); err != nil {
|
||||
cm.server.logger.Error("internal", "couldn't serialize purge record", channel.Name(), err.Error())
|
||||
}
|
||||
// TODO we need a better story about error handling for later
|
||||
if err = cm.server.dstore.Set(datastore.TableChannelPurges, record.UUID, purgeBytes, time.Time{}); err != nil {
|
||||
cm.server.logger.Error("datastore", "couldn't store purge record", chname, err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsPurged queries whether a channel is purged.
|
||||
@ -383,7 +429,7 @@ func (cm *ChannelManager) IsPurged(chname string) (result bool) {
|
||||
}
|
||||
|
||||
cm.RLock()
|
||||
result = cm.purgedChannels.Has(chname)
|
||||
_, result = cm.purgedChannels[chname]
|
||||
cm.RUnlock()
|
||||
return
|
||||
}
|
||||
@ -396,14 +442,16 @@ func (cm *ChannelManager) Unpurge(chname string) (err error) {
|
||||
}
|
||||
|
||||
cm.Lock()
|
||||
found := cm.purgedChannels.Has(chname)
|
||||
record, found := cm.purgedChannels[chname]
|
||||
delete(cm.purgedChannels, chname)
|
||||
cm.Unlock()
|
||||
|
||||
cm.server.channelRegistry.UnpurgeChannel(chname)
|
||||
if !found {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
if err := cm.server.dstore.Delete(datastore.TableChannelPurges, record.UUID); err != nil {
|
||||
cm.server.logger.Error("datastore", "couldn't delete purge record", chname, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -417,3 +465,51 @@ func (cm *ChannelManager) ListPurged() (result []string) {
|
||||
sort.Strings(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
|
||||
cm.RLock()
|
||||
entry := cm.chans[cfname]
|
||||
cm.RUnlock()
|
||||
if entry != nil {
|
||||
return entry.channel.Name()
|
||||
}
|
||||
return cfname
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) LoadPurgeRecord(cfchname string) (record ChannelPurgeRecord, err error) {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
|
||||
if record, ok := cm.purgedChannels[cfchname]; ok {
|
||||
return record, nil
|
||||
} else {
|
||||
return record, errNoSuchChannel
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) ChannelsForAccount(account string) (channels []string) {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
|
||||
for cfname, entry := range cm.chans {
|
||||
if entry.channel.Founder() == account {
|
||||
channels = append(channels, cfname)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AllChannels returns the uncasefolded names of all registered channels.
|
||||
func (cm *ChannelManager) AllRegisteredChannels() (result []string) {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
|
||||
for cfname, entry := range cm.chans {
|
||||
if entry.channel.Founder() != "" {
|
||||
result = append(result, cfname)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -5,62 +5,15 @@ package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// this is exclusively the *persistence* layer for channel registration;
|
||||
// channel creation/tracking/destruction is in channelmanager.go
|
||||
|
||||
const (
|
||||
keyChannelExists = "channel.exists %s"
|
||||
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
||||
keyChannelRegTime = "channel.registered.time %s"
|
||||
keyChannelFounder = "channel.founder %s"
|
||||
keyChannelTopic = "channel.topic %s"
|
||||
keyChannelTopicSetBy = "channel.topic.setby %s"
|
||||
keyChannelTopicSetTime = "channel.topic.settime %s"
|
||||
keyChannelBanlist = "channel.banlist %s"
|
||||
keyChannelExceptlist = "channel.exceptlist %s"
|
||||
keyChannelInvitelist = "channel.invitelist %s"
|
||||
keyChannelPassword = "channel.key %s"
|
||||
keyChannelModes = "channel.modes %s"
|
||||
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
||||
keyChannelUserLimit = "channel.userlimit %s"
|
||||
keyChannelSettings = "channel.settings %s"
|
||||
keyChannelForward = "channel.forward %s"
|
||||
|
||||
keyChannelPurged = "channel.purged %s"
|
||||
)
|
||||
|
||||
var (
|
||||
channelKeyStrings = []string{
|
||||
keyChannelExists,
|
||||
keyChannelName,
|
||||
keyChannelRegTime,
|
||||
keyChannelFounder,
|
||||
keyChannelTopic,
|
||||
keyChannelTopicSetBy,
|
||||
keyChannelTopicSetTime,
|
||||
keyChannelBanlist,
|
||||
keyChannelExceptlist,
|
||||
keyChannelInvitelist,
|
||||
keyChannelPassword,
|
||||
keyChannelModes,
|
||||
keyChannelAccountToUMode,
|
||||
keyChannelUserLimit,
|
||||
keyChannelSettings,
|
||||
keyChannelForward,
|
||||
}
|
||||
)
|
||||
|
||||
// these are bit flags indicating what part of the channel status is "dirty"
|
||||
// and needs to be read from memory and written to the db
|
||||
const (
|
||||
@ -80,8 +33,8 @@ const (
|
||||
type RegisteredChannel struct {
|
||||
// Name of the channel.
|
||||
Name string
|
||||
// Casefolded name of the channel.
|
||||
NameCasefolded string
|
||||
// UUID for the datastore.
|
||||
UUID utils.UUID
|
||||
// RegisteredAt represents the time that the channel was registered.
|
||||
RegisteredAt time.Time
|
||||
// Founder indicates the founder of the channel.
|
||||
@ -110,324 +63,30 @@ type RegisteredChannel struct {
|
||||
Invites map[string]MaskInfo
|
||||
// Settings are the chanserv-modifiable settings
|
||||
Settings ChannelSettings
|
||||
// Metadata set using the METADATA command
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func (r *RegisteredChannel) Deserialize(b []byte) (err error) {
|
||||
return json.Unmarshal(b, r)
|
||||
}
|
||||
|
||||
type ChannelPurgeRecord struct {
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
NameCasefolded string `json:"Name"`
|
||||
UUID utils.UUID
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ChannelRegistry manages registered channels.
|
||||
type ChannelRegistry struct {
|
||||
server *Server
|
||||
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// NewChannelRegistry returns a new ChannelRegistry.
|
||||
func (reg *ChannelRegistry) Initialize(server *Server) {
|
||||
reg.server = server
|
||||
}
|
||||
|
||||
// AllChannels returns the uncasefolded names of all registered channels.
|
||||
func (reg *ChannelRegistry) AllChannels() (result []string) {
|
||||
prefix := fmt.Sprintf(keyChannelName, "")
|
||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
return false
|
||||
}
|
||||
result = append(result, value)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PurgedChannels returns the set of all casefolded channel names that have been purged
|
||||
func (reg *ChannelRegistry) PurgedChannels() (result utils.StringSet) {
|
||||
result = make(utils.StringSet)
|
||||
|
||||
prefix := fmt.Sprintf(keyChannelPurged, "")
|
||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
return false
|
||||
}
|
||||
channel := strings.TrimPrefix(key, prefix)
|
||||
result.Add(channel)
|
||||
return true
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// StoreChannel obtains a consistent view of a channel, then persists it to the store.
|
||||
func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
|
||||
if !reg.server.ChannelRegistrationEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
if info.Founder == "" {
|
||||
// sanity check, don't try to store an unregistered channel
|
||||
return
|
||||
}
|
||||
|
||||
reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
reg.saveChannel(tx, info, includeFlags)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadChannel loads a channel from the store.
|
||||
func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredChannel, err error) {
|
||||
if !reg.server.ChannelRegistrationEnabled() {
|
||||
err = errFeatureDisabled
|
||||
return
|
||||
}
|
||||
|
||||
channelKey := nameCasefolded
|
||||
// nice to have: do all JSON (de)serialization outside of the buntdb transaction
|
||||
err = reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||
_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
|
||||
if dberr == buntdb.ErrNotFound {
|
||||
// chan does not already exist, return
|
||||
return errNoSuchChannel
|
||||
}
|
||||
|
||||
// channel exists, load it
|
||||
name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
|
||||
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
|
||||
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
||||
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
|
||||
topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
|
||||
topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
|
||||
var topicSetTime time.Time
|
||||
topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
|
||||
if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
|
||||
topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
|
||||
}
|
||||
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
|
||||
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
|
||||
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
|
||||
forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
|
||||
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
|
||||
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
||||
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
||||
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
||||
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
|
||||
|
||||
modeSlice := make([]modes.Mode, len(modeString))
|
||||
for i, mode := range modeString {
|
||||
modeSlice[i] = modes.Mode(mode)
|
||||
}
|
||||
|
||||
userLimit, _ := strconv.Atoi(userLimitString)
|
||||
|
||||
var banlist map[string]MaskInfo
|
||||
_ = json.Unmarshal([]byte(banlistString), &banlist)
|
||||
var exceptlist map[string]MaskInfo
|
||||
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
|
||||
var invitelist map[string]MaskInfo
|
||||
_ = json.Unmarshal([]byte(invitelistString), &invitelist)
|
||||
accountToUMode := make(map[string]modes.Mode)
|
||||
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
||||
|
||||
var settings ChannelSettings
|
||||
_ = json.Unmarshal([]byte(settingsString), &settings)
|
||||
|
||||
info = RegisteredChannel{
|
||||
Name: name,
|
||||
NameCasefolded: nameCasefolded,
|
||||
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
|
||||
Founder: founder,
|
||||
Topic: topic,
|
||||
TopicSetBy: topicSetBy,
|
||||
TopicSetTime: topicSetTime,
|
||||
Key: password,
|
||||
Modes: modeSlice,
|
||||
Bans: banlist,
|
||||
Excepts: exceptlist,
|
||||
Invites: invitelist,
|
||||
AccountToUMode: accountToUMode,
|
||||
UserLimit: int(userLimit),
|
||||
Settings: settings,
|
||||
Forward: forward,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Delete deletes a channel corresponding to `info`. If no such channel
|
||||
// is present in the database, no error is returned.
|
||||
func (reg *ChannelRegistry) Delete(info RegisteredChannel) (err error) {
|
||||
if !reg.server.ChannelRegistrationEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
reg.deleteChannel(tx, info.NameCasefolded, info)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete a channel, unless it was overwritten by another registration of the same channel
|
||||
func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info RegisteredChannel) {
|
||||
_, err := tx.Get(fmt.Sprintf(keyChannelExists, key))
|
||||
if err == nil {
|
||||
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
|
||||
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
||||
registeredAt := time.Unix(0, regTimeInt).UTC()
|
||||
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
|
||||
|
||||
// to see if we're deleting the right channel, confirm the founder and the registration time
|
||||
if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) {
|
||||
for _, keyFmt := range channelKeyStrings {
|
||||
tx.Delete(fmt.Sprintf(keyFmt, key))
|
||||
}
|
||||
|
||||
// remove this channel from the client's list of registered channels
|
||||
channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder)
|
||||
channelsStr, err := tx.Get(channelsKey)
|
||||
if err == buntdb.ErrNotFound {
|
||||
return
|
||||
}
|
||||
registeredChannels := unmarshalRegisteredChannels(channelsStr)
|
||||
var nowRegisteredChannels []string
|
||||
for _, channel := range registeredChannels {
|
||||
if channel != key {
|
||||
nowRegisteredChannels = append(nowRegisteredChannels, channel)
|
||||
}
|
||||
}
|
||||
tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) {
|
||||
channelKey := channelInfo.NameCasefolded
|
||||
chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey)
|
||||
founder, existsErr := tx.Get(chanFounderKey)
|
||||
if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder {
|
||||
// add to new founder's list
|
||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder)
|
||||
alreadyChannels, _ := tx.Get(accountChannelsKey)
|
||||
newChannels := channelKey // this is the casefolded channel name
|
||||
if alreadyChannels != "" {
|
||||
newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels)
|
||||
}
|
||||
tx.Set(accountChannelsKey, newChannels, nil)
|
||||
}
|
||||
if existsErr == nil && founder != channelInfo.Founder {
|
||||
// remove from old founder's list
|
||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder)
|
||||
alreadyChannelsRaw, _ := tx.Get(accountChannelsKey)
|
||||
var newChannels []string
|
||||
if alreadyChannelsRaw != "" {
|
||||
for _, chname := range strings.Split(alreadyChannelsRaw, ",") {
|
||||
if chname != channelInfo.NameCasefolded {
|
||||
newChannels = append(newChannels, chname)
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// saveChannel saves a channel to the store.
|
||||
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
|
||||
channelKey := channelInfo.NameCasefolded
|
||||
// maintain the mapping of account -> registered channels
|
||||
reg.updateAccountToChannelMapping(tx, channelInfo)
|
||||
|
||||
if includeFlags&IncludeInitial != 0 {
|
||||
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
||||
}
|
||||
|
||||
if includeFlags&IncludeTopic != 0 {
|
||||
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
|
||||
var topicSetTimeStr string
|
||||
if !channelInfo.TopicSetTime.IsZero() {
|
||||
topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10)
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
|
||||
}
|
||||
|
||||
if includeFlags&IncludeModes != 0 {
|
||||
tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil)
|
||||
modeString := modes.Modes(channelInfo.Modes).String()
|
||||
tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil)
|
||||
}
|
||||
|
||||
if includeFlags&IncludeLists != 0 {
|
||||
banlistString, _ := json.Marshal(channelInfo.Bans)
|
||||
tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil)
|
||||
exceptlistString, _ := json.Marshal(channelInfo.Excepts)
|
||||
tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil)
|
||||
invitelistString, _ := json.Marshal(channelInfo.Invites)
|
||||
tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil)
|
||||
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
|
||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
||||
}
|
||||
|
||||
if includeFlags&IncludeSettings != 0 {
|
||||
settingsString, _ := json.Marshal(channelInfo.Settings)
|
||||
tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// PurgeChannel records a channel purge.
|
||||
func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) {
|
||||
serialized, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serializedStr := string(serialized)
|
||||
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||
|
||||
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(key, serializedStr, nil)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// LoadPurgeRecord retrieves information about whether and how a channel was purged.
|
||||
func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) {
|
||||
var rawRecord string
|
||||
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||
rawRecord, _ = tx.Get(key)
|
||||
return nil
|
||||
})
|
||||
if rawRecord == "" {
|
||||
err = errNoSuchChannel
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(rawRecord), &record)
|
||||
if err != nil {
|
||||
reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error())
|
||||
err = errNoSuchChannel
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnpurgeChannel deletes the record of a channel purge.
|
||||
func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) {
|
||||
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Delete(key)
|
||||
return nil
|
||||
})
|
||||
func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
|
||||
return json.Unmarshal(b, c)
|
||||
}
|
||||
|
||||
140
irc/chanserv.go
140
irc/chanserv.go
@ -6,14 +6,15 @@ package irc
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/sno"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/ergochat/irc-go/ircfmt"
|
||||
)
|
||||
|
||||
const chanservHelp = `ChanServ lets you register and manage channels.`
|
||||
@ -29,7 +30,7 @@ var (
|
||||
help: `Syntax: $bOP #channel [nickname]$b
|
||||
|
||||
OP makes the given nickname, or yourself, a channel admin. You can only use
|
||||
this command if you're the founder of the channel.`,
|
||||
this command if you're a founder or in the AMODEs of the channel.`,
|
||||
helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
|
||||
authRequired: true,
|
||||
enabled: chanregEnabled,
|
||||
@ -79,7 +80,9 @@ AMODE lists or modifies persistent mode settings that affect channel members.
|
||||
For example, $bAMODE #channel +o dan$b grants the holder of the "dan"
|
||||
account the +o operator mode every time they join #channel. To list current
|
||||
accounts and modes, use $bAMODE #channel$b. Note that users are always
|
||||
referenced by their registered account names, not their nicknames.`,
|
||||
referenced by their registered account names, not their nicknames.
|
||||
The permissions hierarchy for adding and removing modes is the same as in
|
||||
the ordinary /MODE command.`,
|
||||
helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
@ -144,7 +147,6 @@ If no regex is provided, all registered channels are returned.`,
|
||||
INFO displays info about a registered channel.`,
|
||||
helpShort: `$bINFO$b displays info about a registered channel.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"get": {
|
||||
handler: csGetHandler,
|
||||
@ -177,7 +179,7 @@ by unprivileged users. Your options are:
|
||||
1. 'none' [no restrictions]
|
||||
2. 'registration-time' [users can view history from after their account was
|
||||
registered, plus a grace period]
|
||||
3. 'join-time' [users can biew history from after they joined the
|
||||
3. 'join-time' [users can view history from after they joined the
|
||||
channel; note that history will be effectively
|
||||
unavailable to clients that are not always-on]
|
||||
4. 'default' [use the server default]`,
|
||||
@ -212,8 +214,17 @@ func csAmodeHandler(service *ircService, server *Server, client *Client, command
|
||||
}
|
||||
|
||||
modeChanges, unknown := modes.ParseChannelModeChanges(params[1:]...)
|
||||
invalid := len(unknown) != 0
|
||||
// #2002: +f takes an argument but is not a channel-user mode,
|
||||
// check for anything valid as a channel mode change that is not valid
|
||||
// as an AMODE change
|
||||
for _, modeChange := range modeChanges {
|
||||
if !slices.Contains(modes.ChannelUserModes, modeChange.Mode) {
|
||||
invalid = true
|
||||
}
|
||||
}
|
||||
var change modes.ModeChange
|
||||
if len(modeChanges) > 1 || len(unknown) > 0 {
|
||||
if len(modeChanges) > 1 || invalid {
|
||||
service.Notice(rb, client.t("Invalid mode change"))
|
||||
return
|
||||
} else if len(modeChanges) == 1 {
|
||||
@ -269,9 +280,13 @@ func csAmodeHandler(service *ircService, server *Server, client *Client, command
|
||||
// #729: apply change to current membership
|
||||
for _, member := range channel.Members() {
|
||||
if member.Account() == change.Arg {
|
||||
applied, change := channel.applyModeToMember(client, change, rb)
|
||||
// applyModeToMember takes the nickname, not the account name,
|
||||
// so translate:
|
||||
modeChange := change
|
||||
modeChange.Arg = member.Nick()
|
||||
applied, modeChange := channel.applyModeToMember(client, modeChange, rb)
|
||||
if applied {
|
||||
announceCmodeChanges(channel, modes.ModeChanges{change}, server.name, "*", "", rb)
|
||||
announceCmodeChanges(channel, modes.ModeChanges{modeChange}, server.name, "*", "", false, rb)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -288,10 +303,11 @@ func csOpHandler(service *ircService, server *Server, client *Client, command st
|
||||
return
|
||||
}
|
||||
channelName := channelInfo.Name()
|
||||
founder := channelInfo.Founder()
|
||||
|
||||
clientAccount := client.Account()
|
||||
if clientAccount == "" || clientAccount != channelInfo.Founder() {
|
||||
service.Notice(rb, client.t("Only the channel founder can do this"))
|
||||
if clientAccount == "" {
|
||||
service.Notice(rb, client.t("You're not logged into an account"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -306,11 +322,26 @@ func csOpHandler(service *ircService, server *Server, client *Client, command st
|
||||
target = client
|
||||
}
|
||||
|
||||
// give them privs
|
||||
givenMode := modes.ChannelOperator
|
||||
if clientAccount == target.Account() {
|
||||
givenMode = modes.ChannelFounder
|
||||
var givenMode modes.Mode
|
||||
if target == client {
|
||||
if clientAccount == founder {
|
||||
givenMode = modes.ChannelFounder
|
||||
} else {
|
||||
givenMode = channelInfo.getAmode(clientAccount)
|
||||
if givenMode == modes.Mode(0) {
|
||||
service.Notice(rb, client.t("You don't have any stored privileges on that channel"))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if clientAccount == founder {
|
||||
givenMode = modes.ChannelOperator
|
||||
} else {
|
||||
service.Notice(rb, client.t("Only the channel founder can do this"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
applied, change := channelInfo.applyModeToMember(client,
|
||||
modes.ModeChange{Mode: givenMode,
|
||||
Op: modes.Add,
|
||||
@ -318,7 +349,7 @@ func csOpHandler(service *ircService, server *Server, client *Client, command st
|
||||
},
|
||||
rb)
|
||||
if applied {
|
||||
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", rb)
|
||||
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", false, rb)
|
||||
}
|
||||
|
||||
service.Notice(rb, client.t("Successfully granted operator privileges"))
|
||||
@ -370,7 +401,8 @@ func csDeopHandler(service *ircService, server *Server, client *Client, command
|
||||
// the changes as coming from chanserv
|
||||
applied := channel.ApplyChannelModeChanges(client, false, modeChanges, rb)
|
||||
details := client.Details()
|
||||
announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, rb)
|
||||
isBot := client.HasMode(modes.Bot)
|
||||
announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, isBot, rb)
|
||||
|
||||
if len(applied) == 0 {
|
||||
return
|
||||
@ -421,14 +453,14 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm
|
||||
},
|
||||
rb)
|
||||
if applied {
|
||||
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", rb)
|
||||
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", false, rb)
|
||||
}
|
||||
}
|
||||
|
||||
// check whether a client has already registered too many channels
|
||||
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
||||
account := client.Account()
|
||||
channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
|
||||
channelsAlreadyRegistered := client.server.channels.ChannelsForAccount(account)
|
||||
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
||||
if !ok {
|
||||
service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||
@ -465,8 +497,8 @@ func csUnregisterHandler(service *ircService, server *Server, client *Client, co
|
||||
return
|
||||
}
|
||||
|
||||
info := channel.ExportRegistration(0)
|
||||
channelKey := info.NameCasefolded
|
||||
info := channel.exportSummary()
|
||||
channelKey := channel.NameCasefolded()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
@ -488,7 +520,7 @@ func csClearHandler(service *ircService, server *Server, client *Client, command
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
if !csPrivsCheck(service, channel.ExportRegistration(0), client, rb) {
|
||||
if !csPrivsCheck(service, channel.exportSummary(), client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -519,17 +551,15 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
regInfo := channel.ExportRegistration(0)
|
||||
regInfo := channel.exportSummary()
|
||||
chname = regInfo.Name
|
||||
account := client.Account()
|
||||
isFounder := account != "" && account == regInfo.Founder
|
||||
var oper *Oper
|
||||
if !isFounder {
|
||||
oper = client.Oper()
|
||||
if !oper.HasRoleCapab("chanreg") {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
oper := client.Oper()
|
||||
hasPrivs := oper.HasRoleCapab("chanreg")
|
||||
if !isFounder && !hasPrivs {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
target := params[1]
|
||||
targetAccount, err := server.accounts.LoadAccount(params[1])
|
||||
@ -551,7 +581,7 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
|
||||
server.snomasks.Send(sno.LocalOpers, message)
|
||||
server.logger.Info("opers", message)
|
||||
}
|
||||
status, err := channel.Transfer(client, target, oper != nil)
|
||||
status, err := channel.Transfer(client, target, hasPrivs)
|
||||
if err == nil {
|
||||
switch status {
|
||||
case channelTransferComplete:
|
||||
@ -563,7 +593,12 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname))
|
||||
}
|
||||
} else {
|
||||
service.Notice(rb, client.t("Could not transfer channel"))
|
||||
switch err {
|
||||
case errChannelNotOwnedByAccount:
|
||||
service.Notice(rb, client.t("You don't own that channel"))
|
||||
default:
|
||||
service.Notice(rb, client.t("Could not transfer channel"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -660,6 +695,7 @@ func csPurgeAddHandler(service *ircService, client *Client, params []string, ope
|
||||
}
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname))
|
||||
client.server.snomasks.Send(sno.LocalChannels, fmt.Sprintf("Operator %s purged channel %s [reason: %s]", operName, chname, reason))
|
||||
case errInvalidChannelName:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname))
|
||||
default:
|
||||
@ -677,6 +713,7 @@ func csPurgeDelHandler(service *ircService, client *Client, params []string, ope
|
||||
switch client.server.channels.Unpurge(chname) {
|
||||
case nil:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
|
||||
client.server.snomasks.Send(sno.LocalChannels, fmt.Sprintf("Operator %s removed purge of channel %s", operName, chname))
|
||||
case errNoSuchChannel:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
|
||||
default:
|
||||
@ -693,11 +730,6 @@ func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer)
|
||||
}
|
||||
|
||||
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if !client.HasRoleCapabs("chanreg") {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
|
||||
var searchRegex *regexp.Regexp
|
||||
if len(params) > 0 {
|
||||
var err error
|
||||
@ -710,7 +742,7 @@ func csListHandler(service *ircService, server *Server, client *Client, command
|
||||
|
||||
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
|
||||
|
||||
channels := server.channelRegistry.AllChannels()
|
||||
channels := server.channels.AllRegisteredChannels()
|
||||
for _, channel := range channels {
|
||||
if searchRegex == nil || searchRegex.MatchString(channel) {
|
||||
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
||||
@ -721,6 +753,12 @@ func csListHandler(service *ircService, server *Server, client *Client, command
|
||||
}
|
||||
|
||||
func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if len(params) == 0 {
|
||||
// #765
|
||||
listRegisteredChannels(service, client.Account(), rb)
|
||||
return
|
||||
}
|
||||
|
||||
chname, err := CasefoldChannel(params[0])
|
||||
if err != nil {
|
||||
service.Notice(rb, client.t("Invalid channel name"))
|
||||
@ -729,7 +767,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
||||
|
||||
// purge status
|
||||
if client.HasRoleCapabs("chanreg") {
|
||||
purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
|
||||
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
|
||||
if err == nil {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
||||
@ -747,13 +785,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
||||
var chinfo RegisteredChannel
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel != nil {
|
||||
chinfo = channel.ExportRegistration(0)
|
||||
} else {
|
||||
chinfo, err = server.channelRegistry.LoadChannel(chname)
|
||||
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
chinfo = channel.exportSummary()
|
||||
}
|
||||
|
||||
// channel exists but is unregistered, or doesn't exist:
|
||||
@ -793,12 +825,12 @@ func csGetHandler(service *ircService, server *Server, client *Client, command s
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
info := channel.ExportRegistration(IncludeSettings)
|
||||
info := channel.exportSummary()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
displayChannelSetting(service, setting, info.Settings, client, rb)
|
||||
displayChannelSetting(service, setting, channel.Settings(), client, rb)
|
||||
}
|
||||
|
||||
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
@ -808,12 +840,12 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
info := channel.ExportRegistration(IncludeSettings)
|
||||
settings := info.Settings
|
||||
info := channel.exportSummary()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
settings := channel.Settings()
|
||||
var err error
|
||||
switch strings.ToLower(setting) {
|
||||
case "history":
|
||||
@ -860,7 +892,7 @@ func csHowToBanHandler(service *ircService, server *Server, client *Client, comm
|
||||
return
|
||||
}
|
||||
|
||||
if !(channel.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("samode")) {
|
||||
if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("samode")) {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
@ -919,7 +951,7 @@ func csHowToBanHandler(service *ircService, server *Server, client *Client, comm
|
||||
success = true
|
||||
if len(collateralDamage) != 0 {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
|
||||
for _, line := range utils.BuildTokenLines(400, collateralDamage, " ") {
|
||||
for _, line := range utils.BuildTokenLines(maxLastArgLength, collateralDamage, " ") {
|
||||
service.Notice(rb, line)
|
||||
}
|
||||
}
|
||||
|
||||
1124
irc/client.go
1124
irc/client.go
File diff suppressed because it is too large
Load Diff
@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks
|
||||
@ -81,30 +81,10 @@ func (clients *ClientManager) Remove(client *Client) error {
|
||||
return clients.removeInternal(client, oldcfnick, oldskeleton)
|
||||
}
|
||||
|
||||
// Handles a RESUME by attaching a session to a designated client. It is the
|
||||
// caller's responsibility to verify that the resume is allowed (checking tokens,
|
||||
// TLS status, etc.) before calling this.
|
||||
func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err error) {
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
cfnick := oldClient.NickCasefolded()
|
||||
if _, ok := clients.byNick[cfnick]; !ok {
|
||||
return errNickMissing
|
||||
}
|
||||
|
||||
success, _, _, _ := oldClient.AddSession(session)
|
||||
if !success {
|
||||
return errNickMissing
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNick sets a client's nickname, validating it against nicknames in use
|
||||
// XXX: dryRun validates a client's ability to claim a nick, without
|
||||
// actually claiming it
|
||||
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, returnedFromAway bool) {
|
||||
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, awayChanged bool) {
|
||||
config := client.server.Config()
|
||||
|
||||
var newCfNick, newSkeleton string
|
||||
@ -114,11 +94,18 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
accountName := client.accountName
|
||||
settings := client.accountSettings
|
||||
registered := client.registered
|
||||
realname := client.realname
|
||||
client.stateMutex.RUnlock()
|
||||
|
||||
if newNick != accountName && strings.ContainsAny(newNick, disfavoredNameCharacters) {
|
||||
return "", errNicknameInvalid, false
|
||||
// these restrictions have grandfather exceptions for nicknames registered
|
||||
// on previous versions of Ergo:
|
||||
if newNick != accountName {
|
||||
// can't contain "disfavored" characters like <, or start with a $ because
|
||||
// it collides with the massmessage mask syntax. '0' conflicts with the use of 0
|
||||
// as a placeholder in WHOX (#1896):
|
||||
if strings.ContainsAny(newNick, disfavoredNameCharacters) || strings.HasPrefix(newNick, "$") ||
|
||||
newNick == "0" {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
}
|
||||
|
||||
// recompute always-on status, because client.alwaysOn is not set for unregistered clients
|
||||
@ -128,8 +115,10 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
|
||||
}
|
||||
|
||||
nickIsReserved := false
|
||||
|
||||
if useAccountName {
|
||||
if registered && newNick != accountName && newNick != "" {
|
||||
if registered && newNick != accountName {
|
||||
return "", errNickAccountMismatch, false
|
||||
}
|
||||
newNick = accountName
|
||||
@ -179,7 +168,9 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
|
||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newCfNick, newSkeleton)
|
||||
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
||||
return "", errNicknameReserved, false
|
||||
// see #2135: we want to enter the critical section, see if the nick is actually in use,
|
||||
// and return errNicknameInUse in that case
|
||||
nickIsReserved = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,31 +198,18 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
dryRun || session == nil {
|
||||
return "", errNicknameInUse, false
|
||||
}
|
||||
// check TLS modes
|
||||
if client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
||||
if useAccountName {
|
||||
// #955: this is fatal because they can't fix it by trying a different nick
|
||||
return "", errInsecureReattach, false
|
||||
} else {
|
||||
return "", errNicknameInUse, false
|
||||
}
|
||||
}
|
||||
reattachSuccessful, numSessions, lastSeen, back := currentClient.AddSession(session)
|
||||
reattachSuccessful, numSessions, lastSeen, wasAway, nowAway := currentClient.AddSession(session)
|
||||
if !reattachSuccessful {
|
||||
return "", errNicknameInUse, false
|
||||
}
|
||||
if numSessions == 1 {
|
||||
invisible := currentClient.HasMode(modes.Invisible)
|
||||
operator := currentClient.HasMode(modes.Operator) || currentClient.HasMode(modes.LocalOperator)
|
||||
operator := currentClient.HasMode(modes.Operator)
|
||||
client.server.stats.AddRegistered(invisible, operator)
|
||||
}
|
||||
session.autoreplayMissedSince = lastSeen
|
||||
// TODO: transition mechanism for #1065, clean this up eventually:
|
||||
if currentClient.Realname() == "" {
|
||||
currentClient.SetRealname(realname)
|
||||
}
|
||||
// successful reattach!
|
||||
return newNick, nil, back
|
||||
return newNick, nil, wasAway != nowAway
|
||||
} else if currentClient == client && currentClient.Nick() == newNick {
|
||||
return "", errNoop, false
|
||||
}
|
||||
@ -240,6 +218,9 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
if skeletonHolder != nil && skeletonHolder != client {
|
||||
return "", errNicknameInUse, false
|
||||
}
|
||||
if nickIsReserved {
|
||||
return "", errNicknameReserved, false
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return "", nil, false
|
||||
@ -267,15 +248,14 @@ func (clients *ClientManager) AllClients() (result []*Client) {
|
||||
return
|
||||
}
|
||||
|
||||
// AllWithCapsNotify returns all clients with the given capabilities, and that support cap-notify.
|
||||
func (clients *ClientManager) AllWithCapsNotify(capabs ...caps.Capability) (sessions []*Session) {
|
||||
capabs = append(capabs, caps.CapNotify)
|
||||
// AllWithCapsNotify returns all sessions that support cap-notify.
|
||||
func (clients *ClientManager) AllWithCapsNotify() (sessions []*Session) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for _, client := range clients.byNick {
|
||||
for _, session := range client.Sessions() {
|
||||
// cap-notify is implicit in cap version 302 and above
|
||||
if session.capabilities.HasAll(capabs...) || 302 <= session.capVersion {
|
||||
if session.capabilities.Has(caps.CapNotify) || 302 <= session.capVersion {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
}
|
||||
@ -284,6 +264,18 @@ func (clients *ClientManager) AllWithCapsNotify(capabs ...caps.Capability) (sess
|
||||
return
|
||||
}
|
||||
|
||||
// AllWithPushSubscriptions returns all clients that are always-on with an active push subscription.
|
||||
func (clients *ClientManager) AllWithPushSubscriptions() (result []*Client) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for _, client := range clients.byNick {
|
||||
if client.hasPushSubscriptions() && client.AlwaysOn() {
|
||||
result = append(result, client)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FindAll returns all clients that match the given userhost mask.
|
||||
func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
@ -308,3 +300,16 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
// Determine the canonical / unfolded form of a nick, if a client matching it
|
||||
// is present (or always-on).
|
||||
func (clients *ClientManager) UnfoldNick(cfnick string) (nick string) {
|
||||
clients.RLock()
|
||||
c := clients.byNick[cfnick]
|
||||
clients.RUnlock()
|
||||
if c != nil {
|
||||
return c.Nick()
|
||||
} else {
|
||||
return cfnick
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,16 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/languages"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
func TestGenerateBatchID(t *testing.T) {
|
||||
var session Session
|
||||
s := make(utils.StringSet)
|
||||
s := make(utils.HashSet[string])
|
||||
|
||||
count := 100000
|
||||
for i := 0; i < count; i++ {
|
||||
@ -30,6 +32,47 @@ func BenchmarkGenerateBatchID(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNames(b *testing.B) {
|
||||
channelSize := 1024
|
||||
server := &Server{
|
||||
name: "ergo.test",
|
||||
}
|
||||
lm, err := languages.NewManager(false, "", "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
server.config.Store(&Config{
|
||||
languageManager: lm,
|
||||
})
|
||||
for i := 0; i < b.N; i++ {
|
||||
channel := &Channel{
|
||||
name: "#test",
|
||||
nameCasefolded: "#test",
|
||||
server: server,
|
||||
members: make(MemberSet),
|
||||
}
|
||||
for j := 0; j < channelSize; j++ {
|
||||
nick := fmt.Sprintf("client_%d", j)
|
||||
client := &Client{
|
||||
server: server,
|
||||
nick: nick,
|
||||
nickCasefolded: nick,
|
||||
}
|
||||
channel.members.Add(client)
|
||||
channel.regenerateMembersCache()
|
||||
session := &Session{
|
||||
client: client,
|
||||
}
|
||||
rb := NewResponseBuffer(session)
|
||||
channel.Names(client, rb)
|
||||
if len(rb.messages) < 2 {
|
||||
b.Fatalf("not enough messages: %d", len(rb.messages))
|
||||
}
|
||||
// to inspect the messages: line, _ := rb.messages[0].Line()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserMasks(t *testing.T) {
|
||||
var um UserMaskSet
|
||||
|
||||
|
||||
@ -126,3 +126,11 @@ func TestAccountCloakCollisions(t *testing.T) {
|
||||
t.Errorf("cloak collision between 97.97.97.97 and aaaa: %s", v4cloak)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAccountCloaks(b *testing.B) {
|
||||
config := cloakConfForTesting()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
config.ComputeAccountCloak("shivaram")
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
"crypto/sha3"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type CloakConfig struct {
|
||||
|
||||
@ -6,22 +6,38 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
// Command represents a command accepted from a client.
|
||||
type Command struct {
|
||||
handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool
|
||||
oper bool
|
||||
handler func(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool
|
||||
usablePreReg bool
|
||||
allowedInBatch bool // allowed in client-to-server batches
|
||||
minParams int
|
||||
capabs []string
|
||||
}
|
||||
|
||||
// resolveCommand returns the command to execute in response to a user input line.
|
||||
// some invalid commands (unknown command verb, invalid UTF8) get a fake handler
|
||||
// to ensure that labeled-response still works as expected.
|
||||
func (server *Server) resolveCommand(command string, invalidUTF8 bool) (canonicalName string, result Command) {
|
||||
if invalidUTF8 {
|
||||
return command, invalidUtf8Command
|
||||
}
|
||||
if cmd, ok := Commands[command]; ok {
|
||||
return command, cmd
|
||||
}
|
||||
if target, ok := server.Config().Server.CommandAliases[command]; ok {
|
||||
if cmd, ok := Commands[target]; ok {
|
||||
return target, cmd
|
||||
}
|
||||
}
|
||||
return command, unknownCommand
|
||||
}
|
||||
|
||||
// Run runs this command with the given client/message.
|
||||
func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.IrcMessage) (exiting bool) {
|
||||
func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.Message) (exiting bool) {
|
||||
rb := NewResponseBuffer(session)
|
||||
rb.Label = GetLabel(msg)
|
||||
|
||||
@ -32,10 +48,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
||||
rb.Add(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
|
||||
return false
|
||||
}
|
||||
if cmd.oper && !client.HasMode(modes.Operator) {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied - You're not an IRC operator"))
|
||||
return false
|
||||
}
|
||||
if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied"))
|
||||
return false
|
||||
@ -59,7 +71,7 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
||||
}
|
||||
|
||||
if client.registered {
|
||||
client.Touch(session)
|
||||
client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp
|
||||
}
|
||||
|
||||
return exiting
|
||||
@ -81,6 +93,10 @@ var Commands map[string]Command
|
||||
|
||||
func init() {
|
||||
Commands = map[string]Command{
|
||||
"ACCEPT": {
|
||||
handler: acceptHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"AMBIANCE": {
|
||||
handler: sceneHandler,
|
||||
minParams: 2,
|
||||
@ -91,18 +107,15 @@ func init() {
|
||||
minParams: 1,
|
||||
},
|
||||
"AWAY": {
|
||||
handler: awayHandler,
|
||||
minParams: 0,
|
||||
handler: awayHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"BATCH": {
|
||||
handler: batchHandler,
|
||||
minParams: 1,
|
||||
allowedInBatch: true,
|
||||
},
|
||||
"BRB": {
|
||||
handler: brbHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"CAP": {
|
||||
handler: capHandler,
|
||||
usablePreReg: true,
|
||||
@ -115,7 +128,7 @@ func init() {
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
capabs: []string{"rehash"},
|
||||
},
|
||||
"DEFCON": {
|
||||
handler: defconHandler,
|
||||
@ -124,12 +137,11 @@ func init() {
|
||||
"DEOPER": {
|
||||
handler: deoperHandler,
|
||||
minParams: 0,
|
||||
oper: true,
|
||||
},
|
||||
"DLINE": {
|
||||
handler: dlineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"EXTJWT": {
|
||||
handler: extjwtHandler,
|
||||
@ -158,6 +170,10 @@ func init() {
|
||||
handler: isonHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"ISUPPORT": {
|
||||
handler: isupportHandler,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"JOIN": {
|
||||
handler: joinHandler,
|
||||
minParams: 1,
|
||||
@ -169,13 +185,12 @@ func init() {
|
||||
"KILL": {
|
||||
handler: killHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
capabs: []string{"kill"},
|
||||
},
|
||||
"KLINE": {
|
||||
handler: klineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"LANGUAGE": {
|
||||
handler: languageHandler,
|
||||
@ -190,6 +205,15 @@ func init() {
|
||||
handler: lusersHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"MARKREAD": {
|
||||
handler: markReadHandler,
|
||||
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
|
||||
},
|
||||
"METADATA": {
|
||||
handler: metadataHandler,
|
||||
minParams: 2,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"MODE": {
|
||||
handler: modeHandler,
|
||||
minParams: 1,
|
||||
@ -237,6 +261,10 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PERSISTENCE": {
|
||||
handler: persistenceHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"PING": {
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
@ -258,18 +286,13 @@ func init() {
|
||||
},
|
||||
"REGISTER": {
|
||||
handler: registerHandler,
|
||||
minParams: 2,
|
||||
minParams: 3,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"RENAME": {
|
||||
handler: renameHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"RESUME": {
|
||||
handler: resumeHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"SAJOIN": {
|
||||
handler: sajoinHandler,
|
||||
minParams: 1,
|
||||
@ -278,7 +301,7 @@ func init() {
|
||||
"SANICK": {
|
||||
handler: sanickHandler,
|
||||
minParams: 2,
|
||||
oper: true,
|
||||
capabs: []string{"samode"},
|
||||
},
|
||||
"SAMODE": {
|
||||
handler: modeHandler,
|
||||
@ -305,10 +328,13 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"REDACT": {
|
||||
handler: redactHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"REHASH": {
|
||||
handler: rehashHandler,
|
||||
minParams: 0,
|
||||
oper: true,
|
||||
capabs: []string{"rehash"},
|
||||
},
|
||||
"TIME": {
|
||||
@ -327,7 +353,7 @@ func init() {
|
||||
"UNDLINE": {
|
||||
handler: unDLineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"UNINVITE": {
|
||||
handler: inviteHandler,
|
||||
@ -336,7 +362,7 @@ func init() {
|
||||
"UNKLINE": {
|
||||
handler: unKLineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"USER": {
|
||||
handler: userHandler,
|
||||
@ -364,6 +390,10 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 4,
|
||||
},
|
||||
"WEBPUSH": {
|
||||
handler: webpushHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"WHO": {
|
||||
handler: whoHandler,
|
||||
minParams: 1,
|
||||
|
||||
528
irc/config.go
528
irc/config.go
@ -8,36 +8,45 @@ package irc
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/ergochat/irc-go/ircfmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/cloaks"
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/email"
|
||||
"github.com/oragono/oragono/irc/isupport"
|
||||
"github.com/oragono/oragono/irc/jwt"
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/mysql"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/cloaks"
|
||||
"github.com/ergochat/ergo/irc/connection_limits"
|
||||
"github.com/ergochat/ergo/irc/custime"
|
||||
"github.com/ergochat/ergo/irc/email"
|
||||
"github.com/ergochat/ergo/irc/isupport"
|
||||
"github.com/ergochat/ergo/irc/jwt"
|
||||
"github.com/ergochat/ergo/irc/languages"
|
||||
"github.com/ergochat/ergo/irc/logger"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/mysql"
|
||||
"github.com/ergochat/ergo/irc/oauth2"
|
||||
"github.com/ergochat/ergo/irc/passwd"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/webpush"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultProxyDeadline = time.Minute
|
||||
)
|
||||
|
||||
// here's how this works: exported (capitalized) members of the config structs
|
||||
@ -54,12 +63,16 @@ type TLSListenConfig struct {
|
||||
|
||||
// This is the YAML-deserializable type of the value of the `Server.Listeners` map
|
||||
type listenerConfigBlock struct {
|
||||
TLS TLSListenConfig
|
||||
Proxy bool
|
||||
Tor bool
|
||||
STSOnly bool `yaml:"sts-only"`
|
||||
WebSocket bool
|
||||
HideSTS bool `yaml:"hide-sts"`
|
||||
// normal TLS configuration, with a single certificate:
|
||||
TLS TLSListenConfig
|
||||
// SNI configuration, with multiple certificates:
|
||||
TLSCertificates []TLSListenConfig `yaml:"tls-certificates"`
|
||||
MinTLSVersion string `yaml:"min-tls-version"`
|
||||
Proxy bool
|
||||
Tor bool
|
||||
STSOnly bool `yaml:"sts-only"`
|
||||
WebSocket bool
|
||||
HideSTS bool `yaml:"hide-sts"`
|
||||
}
|
||||
|
||||
type HistoryCutoff uint
|
||||
@ -162,6 +175,8 @@ func (ps *PersistentStatus) UnmarshalYAML(unmarshal func(interface{}) error) err
|
||||
result = PersistentDisabled
|
||||
}
|
||||
*ps = result
|
||||
} else {
|
||||
err = fmt.Errorf("invalid value `%s` for server persistence status: %w", orig, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -295,6 +310,7 @@ func (t *ThrottleConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err e
|
||||
type AccountConfig struct {
|
||||
Registration AccountRegistrationConfig
|
||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||
AdvertiseSCRAM bool `yaml:"advertise-scram"`
|
||||
RequireSasl struct {
|
||||
Enabled bool
|
||||
Exempted []string
|
||||
@ -322,7 +338,9 @@ type AccountConfig struct {
|
||||
Multiclient MulticlientConfig
|
||||
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
|
||||
VHosts VHostConfig
|
||||
AuthScript AuthScriptConfig `yaml:"auth-script"`
|
||||
AuthScript AuthScriptConfig `yaml:"auth-script"`
|
||||
OAuth2 oauth2.OAuth2BearerConfig `yaml:"oauth2"`
|
||||
JWTAuth jwt.JWTAuthConfig `yaml:"jwt-auth"`
|
||||
}
|
||||
|
||||
type ScriptConfig struct {
|
||||
@ -339,6 +357,11 @@ type AuthScriptConfig struct {
|
||||
Autocreate bool
|
||||
}
|
||||
|
||||
type IPCheckScriptConfig struct {
|
||||
ScriptConfig `yaml:",inline"`
|
||||
ExemptSASL bool `yaml:"exempt-sasl"`
|
||||
}
|
||||
|
||||
// AccountRegistrationConfig controls account registration.
|
||||
type AccountRegistrationConfig struct {
|
||||
Enabled bool
|
||||
@ -416,6 +439,8 @@ func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error
|
||||
method, err := nickReservationFromString(orig)
|
||||
if err == nil {
|
||||
*nr = method
|
||||
} else {
|
||||
err = fmt.Errorf("invalid value `%s` for nick enforcement method: %w", orig, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -434,6 +459,10 @@ func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err err
|
||||
result = CasemappingPRECIS
|
||||
case "permissive", "fun":
|
||||
result = CasemappingPermissive
|
||||
case "rfc1459":
|
||||
result = CasemappingRFC1459
|
||||
case "rfc1459-strict":
|
||||
result = CasemappingRFC1459Strict
|
||||
default:
|
||||
return fmt.Errorf("invalid casemapping value: %s", orig)
|
||||
}
|
||||
@ -468,6 +497,7 @@ type Limits struct {
|
||||
ChanListModes int `yaml:"chan-list-modes"`
|
||||
ChannelLen int `yaml:"channellen"`
|
||||
IdentLen int `yaml:"identlen"`
|
||||
RealnameLen int `yaml:"realnamelen"`
|
||||
KickLen int `yaml:"kicklen"`
|
||||
MonitorEntries int `yaml:"monitor-entries"`
|
||||
NickLen int `yaml:"nicklen"`
|
||||
@ -508,6 +538,7 @@ type FakelagConfig struct {
|
||||
BurstLimit uint `yaml:"burst-limit"`
|
||||
MessagesPerWindow uint `yaml:"messages-per-window"`
|
||||
Cooldown time.Duration
|
||||
CommandBudgets map[string]int `yaml:"command-budgets"`
|
||||
}
|
||||
|
||||
type TorListenersConfig struct {
|
||||
@ -532,11 +563,10 @@ type Config struct {
|
||||
passwordBytes []byte
|
||||
Name string
|
||||
nameCasefolded string
|
||||
// Listeners is the new style for configuring listeners:
|
||||
Listeners map[string]listenerConfigBlock
|
||||
UnixBindMode os.FileMode `yaml:"unix-bind-mode"`
|
||||
TorListeners TorListenersConfig `yaml:"tor-listeners"`
|
||||
WebSockets struct {
|
||||
Listeners map[string]listenerConfigBlock
|
||||
UnixBindMode os.FileMode `yaml:"unix-bind-mode"`
|
||||
TorListeners TorListenersConfig `yaml:"tor-listeners"`
|
||||
WebSockets struct {
|
||||
AllowedOrigins []string `yaml:"allowed-origins"`
|
||||
allowedOriginRegexps []*regexp.Regexp
|
||||
}
|
||||
@ -551,7 +581,12 @@ type Config struct {
|
||||
MOTD string
|
||||
motdLines []string
|
||||
MOTDFormatting bool `yaml:"motd-formatting"`
|
||||
Relaymsg struct {
|
||||
IdleTimeouts struct {
|
||||
Registration time.Duration
|
||||
Ping time.Duration
|
||||
Disconnect time.Duration
|
||||
} `yaml:"idle-timeouts"`
|
||||
Relaymsg struct {
|
||||
Enabled bool
|
||||
Separators string
|
||||
AvailableToChanops bool `yaml:"available-to-chanops"`
|
||||
@ -561,33 +596,50 @@ type Config struct {
|
||||
WebIRC []webircConfig `yaml:"webirc"`
|
||||
MaxSendQString string `yaml:"max-sendq"`
|
||||
MaxSendQBytes int
|
||||
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"`
|
||||
Compatibility struct {
|
||||
ForceTrailing *bool `yaml:"force-trailing"`
|
||||
forceTrailing bool
|
||||
SendUnprefixedSasl bool `yaml:"send-unprefixed-sasl"`
|
||||
SendUnprefixedSasl bool `yaml:"send-unprefixed-sasl"`
|
||||
AllowTruncation *bool `yaml:"allow-truncation"`
|
||||
allowTruncation bool
|
||||
}
|
||||
isupport isupport.List
|
||||
IPLimits connection_limits.LimiterConfig `yaml:"ip-limits"`
|
||||
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
|
||||
SecureNetDefs []string `yaml:"secure-nets"`
|
||||
secureNets []net.IPNet
|
||||
OperThrottle time.Duration `yaml:"oper-throttle"`
|
||||
supportedCaps *caps.Set
|
||||
supportedCapsWithoutSTS *caps.Set
|
||||
capValues caps.Values
|
||||
Casemapping Casemapping
|
||||
EnforceUtf8 bool `yaml:"enforce-utf8"`
|
||||
OutputPath string `yaml:"output-path"`
|
||||
IPCheckScript ScriptConfig `yaml:"ip-check-script"`
|
||||
OverrideServicesHostname string `yaml:"override-services-hostname"`
|
||||
EnforceUtf8 bool `yaml:"enforce-utf8"`
|
||||
OutputPath string `yaml:"output-path"`
|
||||
IPCheckScript IPCheckScriptConfig `yaml:"ip-check-script"`
|
||||
OverrideServicesHostname string `yaml:"override-services-hostname"`
|
||||
MaxLineLen int `yaml:"max-line-len"`
|
||||
SuppressLusers bool `yaml:"suppress-lusers"`
|
||||
AdditionalISupport map[string]string `yaml:"additional-isupport"`
|
||||
CommandAliases map[string]string `yaml:"command-aliases"`
|
||||
}
|
||||
|
||||
API struct {
|
||||
Enabled bool
|
||||
Listener string
|
||||
TLS TLSListenConfig
|
||||
tlsConfig *tls.Config
|
||||
BearerTokens []string `yaml:"bearer-tokens"`
|
||||
bearerTokenBytes [][]byte
|
||||
} `yaml:"api"`
|
||||
|
||||
Roleplay struct {
|
||||
Enabled bool
|
||||
RequireChanops bool `yaml:"require-chanops"`
|
||||
RequireOper bool `yaml:"require-oper"`
|
||||
AddSuffix *bool `yaml:"add-suffix"`
|
||||
addSuffix bool
|
||||
NPCNickMask string `yaml:"npc-nick-mask"`
|
||||
SceneNickMask string `yaml:"scene-nick-mask"`
|
||||
}
|
||||
|
||||
Extjwt struct {
|
||||
@ -603,6 +655,8 @@ type Config struct {
|
||||
|
||||
languageManager *languages.Manager
|
||||
|
||||
LockFile string `yaml:"lock-file"`
|
||||
|
||||
Datastore struct {
|
||||
Path string
|
||||
AutoUpgrade bool
|
||||
@ -623,6 +677,7 @@ type Config struct {
|
||||
}
|
||||
ListDelay time.Duration `yaml:"list-delay"`
|
||||
InviteExpiration custime.Duration `yaml:"invite-expiration"`
|
||||
AutoJoin []string `yaml:"auto-join"`
|
||||
}
|
||||
|
||||
OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`
|
||||
@ -638,7 +693,7 @@ type Config struct {
|
||||
Debug struct {
|
||||
RecoverFromErrors *bool `yaml:"recover-from-errors"`
|
||||
recoverFromErrors bool
|
||||
PprofListener *string `yaml:"pprof-listener"`
|
||||
PprofListener string `yaml:"pprof-listener"`
|
||||
}
|
||||
|
||||
Limits Limits
|
||||
@ -678,14 +733,32 @@ type Config struct {
|
||||
} `yaml:"tagmsg-storage"`
|
||||
}
|
||||
|
||||
Metadata struct {
|
||||
Enabled bool
|
||||
MaxSubs int `yaml:"max-subs"`
|
||||
MaxKeys int `yaml:"max-keys"`
|
||||
MaxValueBytes int `yaml:"max-value-length"`
|
||||
ClientThrottle ThrottleConfig `yaml:"client-throttle"`
|
||||
}
|
||||
|
||||
WebPush struct {
|
||||
Enabled bool
|
||||
Timeout time.Duration
|
||||
Delay time.Duration
|
||||
Subscriber string
|
||||
MaxSubscriptions int `yaml:"max-subscriptions"`
|
||||
Expiration custime.Duration
|
||||
vapidKeys *webpush.VAPIDKeys
|
||||
} `yaml:"webpush"`
|
||||
|
||||
Filename string
|
||||
}
|
||||
|
||||
// OperClass defines an assembled operator class.
|
||||
type OperClass struct {
|
||||
Title string
|
||||
WhoisLine string `yaml:"whois-line"`
|
||||
Capabilities utils.StringSet // map to make lookups much easier
|
||||
WhoisLine string `yaml:"whois-line"`
|
||||
Capabilities utils.HashSet[string] // map to make lookups much easier
|
||||
}
|
||||
|
||||
// OperatorClasses returns a map of assembled operator classes from the given config.
|
||||
@ -723,7 +796,7 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
|
||||
|
||||
// create new operclass
|
||||
var oc OperClass
|
||||
oc.Capabilities = make(utils.StringSet)
|
||||
oc.Capabilities = make(utils.HashSet[string])
|
||||
|
||||
// get inhereted info from other operclasses
|
||||
if len(info.Extends) > 0 {
|
||||
@ -736,6 +809,9 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
|
||||
|
||||
// add our own info
|
||||
oc.Title = info.Title
|
||||
if oc.Title == "" {
|
||||
oc.Title = "IRC operator"
|
||||
}
|
||||
for _, capab := range info.Capabilities {
|
||||
oc.Capabilities.Add(fixupCapability(capab))
|
||||
}
|
||||
@ -816,6 +892,9 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
||||
}
|
||||
|
||||
oper.Vhost = opConf.Vhost
|
||||
if oper.Vhost != "" && !conf.Accounts.VHosts.validRegexp.MatchString(oper.Vhost) {
|
||||
return nil, fmt.Errorf("Oper %s has an invalid vhost: `%s`", name, oper.Vhost)
|
||||
}
|
||||
class, exists := oc[opConf.Class]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Could not load operator [%s] - they use operclass [%s] which does not exist", name, opConf.Class)
|
||||
@ -839,13 +918,30 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
||||
return operators, nil
|
||||
}
|
||||
|
||||
func loadTlsConfig(config TLSListenConfig, webSocket bool) (tlsConfig *tls.Config, err error) {
|
||||
cert, err := tls.LoadX509KeyPair(config.Cert, config.Key)
|
||||
if err != nil {
|
||||
return nil, &CertKeyError{Err: err}
|
||||
func loadTlsConfig(config listenerConfigBlock) (tlsConfig *tls.Config, err error) {
|
||||
var certificates []tls.Certificate
|
||||
if len(config.TLSCertificates) != 0 {
|
||||
// SNI configuration with multiple certificates
|
||||
for _, certPairConf := range config.TLSCertificates {
|
||||
cert, err := loadCertWithLeaf(certPairConf.Cert, certPairConf.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
} else if config.TLS.Cert != "" {
|
||||
// normal configuration with one certificate
|
||||
cert, err := loadCertWithLeaf(config.TLS.Cert, config.TLS.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
} else {
|
||||
// plaintext!
|
||||
return nil, nil
|
||||
}
|
||||
clientAuth := tls.RequestClientCert
|
||||
if webSocket {
|
||||
if config.WebSocket {
|
||||
// if Chrome receives a server request for a client certificate
|
||||
// on a websocket connection, it will immediately disconnect:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=329884
|
||||
@ -853,12 +949,45 @@ func loadTlsConfig(config TLSListenConfig, webSocket bool) (tlsConfig *tls.Confi
|
||||
clientAuth = tls.NoClientCert
|
||||
}
|
||||
result := tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
Certificates: certificates,
|
||||
ClientAuth: clientAuth,
|
||||
MinVersion: tlsMinVersionFromString(config.MinTLSVersion),
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func tlsMinVersionFromString(version string) uint16 {
|
||||
version = strings.ToLower(version)
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
switch version {
|
||||
case "1", "1.0":
|
||||
return tls.VersionTLS10
|
||||
case "1.1":
|
||||
return tls.VersionTLS11
|
||||
case "1.2":
|
||||
return tls.VersionTLS12
|
||||
case "1.3":
|
||||
return tls.VersionTLS13
|
||||
default:
|
||||
// tls package will fill in a sane value, currently 1.0
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func loadCertWithLeaf(certFile, keyFile string) (cert tls.Certificate, err error) {
|
||||
// LoadX509KeyPair: "On successful return, Certificate.Leaf will be nil because
|
||||
// the parsed form of the certificate is not retained." tls.Config:
|
||||
// "Note: if there are multiple Certificates, and they don't have the
|
||||
// optional field Leaf set, certificate selection will incur a significant
|
||||
// per-handshake performance cost."
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
|
||||
return
|
||||
}
|
||||
|
||||
// prepareListeners populates Config.Server.trueListeners
|
||||
func (conf *Config) prepareListeners() (err error) {
|
||||
if len(conf.Server.Listeners) == 0 {
|
||||
@ -868,18 +997,15 @@ func (conf *Config) prepareListeners() (err error) {
|
||||
conf.Server.trueListeners = make(map[string]utils.ListenerConfig)
|
||||
for addr, block := range conf.Server.Listeners {
|
||||
var lconf utils.ListenerConfig
|
||||
lconf.ProxyDeadline = RegisterTimeout
|
||||
lconf.ProxyDeadline = defaultProxyDeadline
|
||||
lconf.Tor = block.Tor
|
||||
lconf.STSOnly = block.STSOnly
|
||||
if lconf.STSOnly && !conf.Server.STS.Enabled {
|
||||
return fmt.Errorf("%s is configured as a STS-only listener, but STS is disabled", addr)
|
||||
}
|
||||
if block.TLS.Cert != "" {
|
||||
tlsConfig, err := loadTlsConfig(block.TLS, block.WebSocket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lconf.TLSConfig = tlsConfig
|
||||
lconf.TLSConfig, err = loadTlsConfig(block)
|
||||
if err != nil {
|
||||
return &CertKeyError{Err: err}
|
||||
}
|
||||
lconf.RequireProxy = block.TLS.Proxy || block.Proxy
|
||||
lconf.WebSocket = block.WebSocket
|
||||
@ -915,9 +1041,43 @@ func (config *Config) processExtjwt() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *Config) processAPI() (err error) {
|
||||
if !config.API.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.API.Listener == "" {
|
||||
return errors.New("config.api.enabled is true, but listener address is empty")
|
||||
}
|
||||
|
||||
config.API.bearerTokenBytes = make([][]byte, len(config.API.BearerTokens))
|
||||
for i, tok := range config.API.BearerTokens {
|
||||
if tok == "" || tok == "example" {
|
||||
continue
|
||||
}
|
||||
config.API.bearerTokenBytes[i] = []byte(tok)
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if config.API.TLS.Cert != "" {
|
||||
cert, err := loadCertWithLeaf(config.API.TLS.Cert, config.API.TLS.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// TODO consider supporting client certificates
|
||||
}
|
||||
}
|
||||
config.API.tlsConfig = tlsConfig
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadRawConfig loads the config without doing any consistency checks or postprocessing
|
||||
func LoadRawConfig(filename string) (config *Config, err error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -964,13 +1124,16 @@ func (ce *configPathError) Error() string {
|
||||
return fmt.Sprintf("Couldn't apply config override `%s`: %s", ce.name, ce.desc)
|
||||
}
|
||||
|
||||
func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *configPathError) {
|
||||
func mungeFromEnvironment(config *Config, envPair string) (applied bool, name string, err *configPathError) {
|
||||
equalIdx := strings.IndexByte(envPair, '=')
|
||||
name, value := envPair[:equalIdx], envPair[equalIdx+1:]
|
||||
if !strings.HasPrefix(name, "ORAGONO__") {
|
||||
return false, nil
|
||||
if strings.HasPrefix(name, "ERGO__") {
|
||||
name = strings.TrimPrefix(name, "ERGO__")
|
||||
} else if strings.HasPrefix(name, "ORAGONO__") {
|
||||
name = strings.TrimPrefix(name, "ORAGONO__")
|
||||
} else {
|
||||
return false, "", nil
|
||||
}
|
||||
name = strings.TrimPrefix(name, "ORAGONO__")
|
||||
pathComponents := strings.Split(name, "__")
|
||||
for i, pathComponent := range pathComponents {
|
||||
pathComponents[i] = screamingSnakeToKebab(pathComponent)
|
||||
@ -980,10 +1143,10 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *co
|
||||
t := v.Type()
|
||||
for _, component := range pathComponents {
|
||||
if component == "" {
|
||||
return false, &configPathError{name, "invalid", nil}
|
||||
return false, "", &configPathError{name, "invalid", nil}
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return false, &configPathError{name, "index into non-struct", nil}
|
||||
return false, "", &configPathError{name, "index into non-struct", nil}
|
||||
}
|
||||
var nextField reflect.StructField
|
||||
success := false
|
||||
@ -1009,7 +1172,7 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *co
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
return false, &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
||||
return false, "", &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
||||
}
|
||||
v = v.FieldByName(nextField.Name)
|
||||
// dereference pointer field if necessary, initialize new value if necessary
|
||||
@ -1023,9 +1186,9 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *co
|
||||
}
|
||||
yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
|
||||
if yamlErr != nil {
|
||||
return false, &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
||||
return false, "", &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
||||
}
|
||||
return true, nil
|
||||
return true, name, nil
|
||||
}
|
||||
|
||||
// LoadConfig loads the given YAML configuration file.
|
||||
@ -1037,7 +1200,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
|
||||
if config.AllowEnvironmentOverrides {
|
||||
for _, envPair := range os.Environ() {
|
||||
applied, envErr := mungeFromEnvironment(config, envPair)
|
||||
applied, name, envErr := mungeFromEnvironment(config, envPair)
|
||||
if envErr != nil {
|
||||
if envErr.fatalErr != nil {
|
||||
return nil, envErr
|
||||
@ -1045,7 +1208,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
log.Println(envErr.Error())
|
||||
}
|
||||
} else if applied {
|
||||
log.Printf("applied environment override: %s\n", envPair)
|
||||
log.Printf("applied environment override: %s\n", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1075,12 +1238,32 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
if config.Limits.RegistrationMessages == 0 {
|
||||
config.Limits.RegistrationMessages = 1024
|
||||
}
|
||||
if config.Server.MaxLineLen < DefaultMaxLineLen {
|
||||
config.Server.MaxLineLen = DefaultMaxLineLen
|
||||
}
|
||||
if config.Datastore.MySQL.Enabled {
|
||||
if config.Limits.NickLen > mysql.MaxTargetLength || config.Limits.ChannelLen > mysql.MaxTargetLength {
|
||||
return nil, fmt.Errorf("to use MySQL, nick and channel length limits must be %d or lower", mysql.MaxTargetLength)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Server.IdleTimeouts.Registration <= 0 {
|
||||
config.Server.IdleTimeouts.Registration = time.Minute
|
||||
}
|
||||
if config.Server.IdleTimeouts.Ping <= 0 {
|
||||
config.Server.IdleTimeouts.Ping = time.Minute + 30*time.Second
|
||||
}
|
||||
if config.Server.IdleTimeouts.Disconnect <= 0 {
|
||||
config.Server.IdleTimeouts.Disconnect = 2*time.Minute + 30*time.Second
|
||||
}
|
||||
|
||||
if !(config.Server.IdleTimeouts.Ping < config.Server.IdleTimeouts.Disconnect) {
|
||||
return nil, fmt.Errorf(
|
||||
"ping timeout %v must be strictly less than disconnect timeout %v, to give the client time to respond",
|
||||
config.Server.IdleTimeouts.Ping, config.Server.IdleTimeouts.Disconnect,
|
||||
)
|
||||
}
|
||||
|
||||
if config.Server.CoerceIdent != "" {
|
||||
if config.Server.CheckIdent {
|
||||
return nil, errors.New("Can't configure both check-ident and coerce-ident")
|
||||
@ -1262,6 +1445,25 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
|
||||
config.Accounts.defaultUserModes = ParseDefaultUserModes(config.Accounts.DefaultUserModes)
|
||||
|
||||
if config.Server.Password != "" {
|
||||
config.Server.passwordBytes, err = decodeLegacyPasswordHash(config.Server.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Accounts.LoginViaPassCommand && !config.Accounts.SkipServerPassword {
|
||||
return nil, errors.New("Using a server password and login-via-pass-command requires skip-server-password as well")
|
||||
}
|
||||
// #1634: accounts.registration.allow-before-connect is an auth bypass
|
||||
// for configurations that start from default and then enable server.password
|
||||
config.Accounts.Registration.AllowBeforeConnect = false
|
||||
}
|
||||
|
||||
if config.Accounts.RequireSasl.Enabled {
|
||||
// minor gotcha: Tor listeners will typically be loopback and
|
||||
// therefore exempted from require-sasl. if require-sasl is enabled
|
||||
// for non-Tor (non-local) connections, enable it for Tor as well:
|
||||
config.Server.TorListeners.RequireSasl = true
|
||||
}
|
||||
config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())
|
||||
@ -1290,13 +1492,40 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
|
||||
}
|
||||
|
||||
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL"
|
||||
if !config.Accounts.AuthenticationEnabled {
|
||||
if config.Accounts.AuthenticationEnabled {
|
||||
saslCapValues := []string{"PLAIN", "EXTERNAL"}
|
||||
if config.Accounts.AdvertiseSCRAM {
|
||||
saslCapValues = append(saslCapValues, "SCRAM-SHA-256")
|
||||
}
|
||||
if config.Accounts.OAuth2.Enabled {
|
||||
saslCapValues = append(saslCapValues, "OAUTHBEARER")
|
||||
}
|
||||
if config.Accounts.OAuth2.Enabled || config.Accounts.JWTAuth.Enabled {
|
||||
saslCapValues = append(saslCapValues, "IRCV3BEARER")
|
||||
}
|
||||
config.Server.capValues[caps.SASL] = strings.Join(saslCapValues, ",")
|
||||
} else {
|
||||
config.Server.supportedCaps.Disable(caps.SASL)
|
||||
}
|
||||
|
||||
if config.Server.OperThrottle <= 0 {
|
||||
config.Server.OperThrottle = 10 * time.Second
|
||||
}
|
||||
|
||||
if err := config.Accounts.OAuth2.Postprocess(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.Accounts.JWTAuth.Postprocess(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Accounts.OAuth2.Enabled && config.Accounts.OAuth2.AuthScript && !config.Accounts.AuthScript.Enabled {
|
||||
return nil, fmt.Errorf("oauth2 is enabled with auth-script, but no auth-script is enabled")
|
||||
}
|
||||
|
||||
if !config.Accounts.Registration.Enabled {
|
||||
config.Server.supportedCaps.Disable(caps.Register)
|
||||
config.Server.supportedCaps.Disable(caps.AccountRegistration)
|
||||
} else {
|
||||
var registerValues []string
|
||||
if config.Accounts.Registration.AllowBeforeConnect {
|
||||
@ -1309,7 +1538,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
registerValues = append(registerValues, "account-required")
|
||||
}
|
||||
if len(registerValues) != 0 {
|
||||
config.Server.capValues[caps.Register] = strings.Join(registerValues, ",")
|
||||
config.Server.capValues[caps.AccountRegistration] = strings.Join(registerValues, ",")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1325,6 +1554,17 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
config.Server.capValues[caps.Languages] = config.languageManager.CapValue()
|
||||
|
||||
if len(config.Fakelag.CommandBudgets) != 0 {
|
||||
// normalize command names to uppercase:
|
||||
commandBudgets := make(map[string]int, len(config.Fakelag.CommandBudgets))
|
||||
for command, budget := range config.Fakelag.CommandBudgets {
|
||||
commandBudgets[strings.ToUpper(command)] = budget
|
||||
}
|
||||
config.Fakelag.CommandBudgets = commandBudgets
|
||||
} else {
|
||||
config.Fakelag.CommandBudgets = nil
|
||||
}
|
||||
|
||||
if config.Server.Relaymsg.Enabled {
|
||||
for _, char := range protocolBreakingNameCharacters {
|
||||
if strings.ContainsRune(config.Server.Relaymsg.Separators, char) {
|
||||
@ -1352,16 +1592,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
// parse default channel modes
|
||||
config.Channels.defaultModes = ParseDefaultChannelModes(config.Channels.DefaultModes)
|
||||
|
||||
if config.Server.Password != "" {
|
||||
config.Server.passwordBytes, err = decodeLegacyPasswordHash(config.Server.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Accounts.LoginViaPassCommand && !config.Accounts.SkipServerPassword {
|
||||
return nil, errors.New("Using a server password and login-via-pass-command requires skip-server-password as well")
|
||||
}
|
||||
}
|
||||
|
||||
if config.Accounts.Registration.BcryptCost == 0 {
|
||||
config.Accounts.Registration.BcryptCost = passwd.DefaultCost
|
||||
}
|
||||
@ -1374,15 +1604,20 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
|
||||
config.Server.Compatibility.forceTrailing = utils.BoolDefaultTrue(config.Server.Compatibility.ForceTrailing)
|
||||
config.Server.Compatibility.allowTruncation = utils.BoolDefaultTrue(config.Server.Compatibility.AllowTruncation)
|
||||
|
||||
config.loadMOTD()
|
||||
|
||||
// in the current implementation, we disable history by creating a history buffer
|
||||
// with zero capacity. but the `enabled` config option MUST be respected regardless
|
||||
// of this detail
|
||||
if !config.History.Enabled {
|
||||
if !config.History.Enabled || config.History.ChathistoryMax == 0 {
|
||||
config.History.ChannelLength = 0
|
||||
config.History.ClientLength = 0
|
||||
config.Server.supportedCaps.Disable(caps.Chathistory)
|
||||
config.Server.supportedCaps.Disable(caps.EventPlayback)
|
||||
config.Server.supportedCaps.Disable(caps.ZNCPlayback)
|
||||
config.Server.supportedCaps.Disable(caps.MessageRedaction)
|
||||
}
|
||||
|
||||
if !config.History.Enabled || !config.History.Persistent.Enabled {
|
||||
@ -1413,10 +1648,26 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if !config.History.Retention.AllowIndividualDelete {
|
||||
config.Server.supportedCaps.Disable(caps.MessageRedaction) // #2215
|
||||
}
|
||||
|
||||
config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix)
|
||||
if config.Roleplay.NPCNickMask == "" {
|
||||
config.Roleplay.NPCNickMask = defaultNPCNickMask
|
||||
}
|
||||
if config.Roleplay.SceneNickMask == "" {
|
||||
config.Roleplay.SceneNickMask = defaultSceneNickMask
|
||||
}
|
||||
|
||||
config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
|
||||
config.Datastore.MySQL.TrackAccountMessages = config.History.Retention.EnableAccountIndexing
|
||||
if config.Datastore.MySQL.MaxConns == 0 {
|
||||
// #1622: not putting an upper limit on the number of MySQL connections is
|
||||
// potentially dangerous. as a naive heuristic, assume they're running on the
|
||||
// same machine:
|
||||
config.Datastore.MySQL.MaxConns = runtime.NumCPU()
|
||||
}
|
||||
|
||||
config.Server.Cloaks.Initialize()
|
||||
if config.Server.Cloaks.Enabled {
|
||||
@ -1425,22 +1676,71 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if !config.Metadata.Enabled {
|
||||
config.Server.supportedCaps.Disable(caps.Metadata)
|
||||
} else {
|
||||
metadataValues := make([]string, 0, 4)
|
||||
metadataValues = append(metadataValues, "before-connect")
|
||||
// these are required for normal operation, so set sane defaults:
|
||||
if config.Metadata.MaxSubs == 0 {
|
||||
config.Metadata.MaxSubs = 10
|
||||
}
|
||||
metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs))
|
||||
if config.Metadata.MaxKeys == 0 {
|
||||
config.Metadata.MaxKeys = 10
|
||||
}
|
||||
metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys))
|
||||
// this is not required since we enforce a hardcoded upper bound on key+value
|
||||
if config.Metadata.MaxValueBytes > 0 {
|
||||
metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes))
|
||||
}
|
||||
config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",")
|
||||
}
|
||||
|
||||
err = config.processExtjwt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.WebPush.Enabled {
|
||||
if config.Accounts.Multiclient.AlwaysOn == PersistentDisabled {
|
||||
return nil, fmt.Errorf("Cannot enable webpush if always-on is disabled")
|
||||
}
|
||||
if config.WebPush.Timeout == 0 {
|
||||
config.WebPush.Timeout = 10 * time.Second
|
||||
}
|
||||
if config.WebPush.Subscriber == "" {
|
||||
config.WebPush.Subscriber = "https://ergo.chat/about"
|
||||
}
|
||||
if config.WebPush.MaxSubscriptions <= 0 {
|
||||
config.WebPush.MaxSubscriptions = 1
|
||||
}
|
||||
if config.WebPush.Expiration == 0 {
|
||||
config.WebPush.Expiration = custime.Duration(14 * 24 * time.Hour)
|
||||
} else if config.WebPush.Expiration < custime.Duration(3*24*time.Hour) {
|
||||
return nil, fmt.Errorf("webpush.expiration is too short (should be several days)")
|
||||
}
|
||||
} else {
|
||||
config.Server.supportedCaps.Disable(caps.WebPush)
|
||||
config.Server.supportedCaps.Disable(caps.SojuWebPush)
|
||||
}
|
||||
|
||||
err = config.processAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Server.CommandAliases, err = normalizeCommandAliases(config.Server.CommandAliases)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now that all postprocessing is complete, regenerate ISUPPORT:
|
||||
err = config.generateISupport()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = config.prepareListeners()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare listeners: %v", err)
|
||||
}
|
||||
|
||||
// #1428: Tor listeners should never see STS
|
||||
config.Server.supportedCapsWithoutSTS = caps.NewSet()
|
||||
config.Server.supportedCapsWithoutSTS.Union(config.Server.supportedCaps)
|
||||
@ -1458,6 +1758,10 @@ func (config *Config) isRelaymsgIdentifier(nick string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(nick, "#") {
|
||||
return false // #2114
|
||||
}
|
||||
|
||||
for _, char := range config.Server.Relaymsg.Separators {
|
||||
if strings.ContainsRune(nick, char) {
|
||||
return true
|
||||
@ -1475,10 +1779,21 @@ func (config *Config) generateISupport() (err error) {
|
||||
isupport.Initialize()
|
||||
isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
|
||||
isupport.Add("BOT", "B")
|
||||
isupport.Add("CASEMAPPING", "ascii")
|
||||
var casemappingToken string
|
||||
switch config.Server.Casemapping {
|
||||
default:
|
||||
casemappingToken = "ascii" // this is published for ascii, precis, or permissive
|
||||
case CasemappingRFC1459:
|
||||
casemappingToken = "rfc1459"
|
||||
case CasemappingRFC1459Strict:
|
||||
casemappingToken = "rfc1459-strict"
|
||||
}
|
||||
isupport.Add("CASEMAPPING", casemappingToken)
|
||||
isupport.Add("CHANLIMIT", fmt.Sprintf("%s:%d", chanTypes, config.Channels.MaxChannelsPerClient))
|
||||
isupport.Add("CHANMODES", chanmodesToken)
|
||||
isupport.Add("CHANMODES", modes.ChanmodesToken())
|
||||
if config.History.Enabled && config.History.ChathistoryMax > 0 {
|
||||
isupport.Add("CHATHISTORY", strconv.Itoa(config.History.ChathistoryMax))
|
||||
// Kiwi expects this legacy token name:
|
||||
isupport.Add("draft/CHATHISTORY", strconv.Itoa(config.History.ChathistoryMax))
|
||||
}
|
||||
isupport.Add("CHANNELLEN", strconv.Itoa(config.Limits.ChannelLen))
|
||||
@ -1489,10 +1804,12 @@ func (config *Config) generateISupport() (err error) {
|
||||
isupport.Add("EXTJWT", "1")
|
||||
}
|
||||
isupport.Add("EXTBAN", ",m")
|
||||
isupport.Add("FORWARD", "f")
|
||||
isupport.Add("INVEX", "")
|
||||
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
||||
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
||||
isupport.Add("MAXTARGETS", maxTargetsString)
|
||||
isupport.Add("MSGREFTYPES", "msgid,timestamp")
|
||||
isupport.Add("MODES", "")
|
||||
isupport.Add("MONITOR", strconv.Itoa(config.Limits.MonitorEntries))
|
||||
isupport.Add("NETWORK", config.Network.Name)
|
||||
@ -1502,8 +1819,10 @@ func (config *Config) generateISupport() (err error) {
|
||||
isupport.Add("RPCHAN", "E")
|
||||
isupport.Add("RPUSER", "E")
|
||||
}
|
||||
isupport.Add("SAFELIST", "")
|
||||
isupport.Add("SAFERATE", "")
|
||||
isupport.Add("STATUSMSG", "~&@%+")
|
||||
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
|
||||
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
|
||||
isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
|
||||
if config.Server.Casemapping == CasemappingPRECIS {
|
||||
isupport.Add("UTF8MAPPING", precisUTF8MappingToken)
|
||||
@ -1511,8 +1830,21 @@ func (config *Config) generateISupport() (err error) {
|
||||
if config.Server.EnforceUtf8 {
|
||||
isupport.Add("UTF8ONLY", "")
|
||||
}
|
||||
if config.WebPush.Enabled {
|
||||
// XXX we typically don't have this at config parse time, so we'll have to regenerate
|
||||
// the cached reply later
|
||||
if config.WebPush.vapidKeys != nil {
|
||||
isupport.Add("VAPID", config.WebPush.vapidKeys.PublicKeyString())
|
||||
}
|
||||
}
|
||||
isupport.Add("WHOX", "")
|
||||
|
||||
for key, value := range config.Server.AdditionalISupport {
|
||||
if !isupport.Contains(key) {
|
||||
isupport.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
err = isupport.RegenerateCachedReply()
|
||||
return
|
||||
}
|
||||
@ -1598,7 +1930,7 @@ func (config *Config) loadMOTD() error {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
contents, err := ioutil.ReadAll(file)
|
||||
contents, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1614,6 +1946,9 @@ func (config *Config) loadMOTD() error {
|
||||
if config.Server.MOTDFormatting {
|
||||
lineToSend = ircfmt.Unescape(lineToSend)
|
||||
}
|
||||
if config.Server.EnforceUtf8 && !utf8.ValidString(lineToSend) {
|
||||
return fmt.Errorf("Line %d of MOTD contains invalid UTF8", i+1)
|
||||
}
|
||||
// "- " is the required prefix for MOTD
|
||||
lineToSend = fmt.Sprintf("- %s", lineToSend)
|
||||
config.Server.motdLines = append(config.Server.motdLines, lineToSend)
|
||||
@ -1621,3 +1956,22 @@ func (config *Config) loadMOTD() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeCommandAliases(aliases map[string]string) (normalizedAliases map[string]string, err error) {
|
||||
if len(aliases) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
normalizedAliases = make(map[string]string, len(aliases))
|
||||
for alias, command := range aliases {
|
||||
alias = strings.ToUpper(alias)
|
||||
command = strings.ToUpper(command)
|
||||
if _, found := Commands[alias]; found {
|
||||
return nil, fmt.Errorf("Command alias `%s` collides with a real Ergo command", alias)
|
||||
}
|
||||
if _, found := Commands[command]; !found {
|
||||
return nil, fmt.Errorf("Command alias `%s` mapped to non-existent Ergo command `%s`", alias, command)
|
||||
}
|
||||
normalizedAliases[alias] = command
|
||||
}
|
||||
return normalizedAliases, nil
|
||||
}
|
||||
|
||||
@ -19,16 +19,16 @@ func TestEnvironmentOverrides(t *testing.T) {
|
||||
env := []string{
|
||||
`USER=shivaram`, // unrelated var
|
||||
`ORAGONO_USER=oragono`, // this should be ignored as well
|
||||
`ORAGONO__NETWORK__NAME=example.com`,
|
||||
`ERGO__NETWORK__NAME=example.com`,
|
||||
`ORAGONO__SERVER__COMPATIBILITY__FORCE_TRAILING=false`,
|
||||
`ORAGONO__SERVER__COERCE_IDENT="~user"`,
|
||||
`ORAGONO__SERVER__MOTD=short.motd.txt`,
|
||||
`ERGO__SERVER__MOTD=short.motd.txt`,
|
||||
`ORAGONO__ACCOUNTS__NICK_RESERVATION__ENABLED=true`,
|
||||
`ORAGONO__ACCOUNTS__DEFAULT_USER_MODES="+iR"`,
|
||||
`ERGO__ACCOUNTS__DEFAULT_USER_MODES="+iR"`,
|
||||
`ORAGONO__SERVER__IP_CLOAKING={"enabled": true, "enabled-for-always-on": true, "netname": "irc", "cidr-len-ipv4": 32, "cidr-len-ipv6": 64, "num-bits": 64}`,
|
||||
}
|
||||
for _, envPair := range env {
|
||||
_, err := mungeFromEnvironment(&config, envPair)
|
||||
_, _, err := mungeFromEnvironment(&config, envPair)
|
||||
if err != nil {
|
||||
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
|
||||
}
|
||||
@ -93,7 +93,7 @@ func TestEnvironmentOverrideErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, env := range invalidEnvs {
|
||||
success, err := mungeFromEnvironment(&config, env)
|
||||
success, _, err := mungeFromEnvironment(&config, env)
|
||||
if err == nil || success {
|
||||
t.Errorf("accepted invalid env override `%s`", env)
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/flatip"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/flatip"
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
)
|
||||
|
||||
func easyParseIP(ipstr string) (result flatip.IP) {
|
||||
|
||||
320
irc/database.go
320
irc/database.go
@ -14,19 +14,31 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/bunt"
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/webpush"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
// 'version' of the database schema
|
||||
keySchemaVersion = "db.version"
|
||||
// latest schema of the db
|
||||
latestDbSchema = 20
|
||||
// TODO migrate metadata keys as well
|
||||
|
||||
keyCloakSecret = "crypto.cloak_secret"
|
||||
// 'version' of the database schema
|
||||
// latest schema of the db
|
||||
latestDbSchema = 24
|
||||
)
|
||||
|
||||
var (
|
||||
schemaVersionUUID = utils.UUID{0, 255, 85, 13, 212, 10, 191, 121, 245, 152, 142, 89, 97, 141, 219, 87} // AP9VDdQKv3n1mI5ZYY3bVw
|
||||
cloakSecretUUID = utils.UUID{170, 214, 184, 208, 116, 181, 67, 75, 161, 23, 233, 16, 113, 251, 94, 229} // qta40HS1Q0uhF-kQcfte5Q
|
||||
vapidKeysUUID = utils.UUID{87, 215, 189, 5, 65, 105, 249, 44, 65, 96, 170, 56, 187, 110, 12, 235} // V9e9BUFp-SxBYKo4u24M6w
|
||||
|
||||
keySchemaVersion = bunt.BuntKey(datastore.TableMetadata, schemaVersionUUID)
|
||||
keyCloakSecret = bunt.BuntKey(datastore.TableMetadata, cloakSecretUUID)
|
||||
keyVAPIDKeys = bunt.BuntKey(datastore.TableMetadata, vapidKeysUUID)
|
||||
)
|
||||
|
||||
type SchemaChanger func(*Config, *buntdb.Tx) error
|
||||
@ -71,6 +83,15 @@ func initializeDB(path string) error {
|
||||
// set schema version
|
||||
tx.Set(keySchemaVersion, strconv.Itoa(latestDbSchema), nil)
|
||||
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
|
||||
vapidKeys, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j, err := json.Marshal(vapidKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Set(keyVAPIDKeys, string(j), nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -99,10 +120,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
||||
// read the current version string
|
||||
var version int
|
||||
err = db.View(func(tx *buntdb.Tx) (err error) {
|
||||
vStr, err := tx.Get(keySchemaVersion)
|
||||
if err == nil {
|
||||
version, err = strconv.Atoi(vStr)
|
||||
}
|
||||
version, err = retrieveSchemaVersion(tx)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@ -130,10 +148,21 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveSchemaVersion(tx *buntdb.Tx) (version int, err error) {
|
||||
if val, err := tx.Get(keySchemaVersion); err == nil {
|
||||
return strconv.Atoi(val)
|
||||
}
|
||||
// legacy key:
|
||||
if val, err := tx.Get("db.version"); err == nil {
|
||||
return strconv.Atoi(val)
|
||||
}
|
||||
return 0, buntdb.ErrNotFound
|
||||
}
|
||||
|
||||
func performAutoUpgrade(currentVersion int, config *Config) (err error) {
|
||||
path := config.Datastore.Path
|
||||
log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
|
||||
timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
|
||||
timestamp := time.Now().UTC().Format("2006-01-02-15.04.05.000Z")
|
||||
backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp)
|
||||
log.Printf("making a backup of current database at %s\n", backupPath)
|
||||
err = utils.CopyFile(path, backupPath)
|
||||
@ -167,8 +196,12 @@ func UpgradeDB(config *Config) (err error) {
|
||||
var version int
|
||||
err = store.Update(func(tx *buntdb.Tx) error {
|
||||
for {
|
||||
vStr, _ := tx.Get(keySchemaVersion)
|
||||
version, _ = strconv.Atoi(vStr)
|
||||
if version == 0 {
|
||||
version, err = retrieveSchemaVersion(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if version == latestDbSchema {
|
||||
// success!
|
||||
break
|
||||
@ -183,11 +216,12 @@ func UpgradeDB(config *Config) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
|
||||
version = change.TargetVersion
|
||||
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(version), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
|
||||
log.Printf("successfully updated schema to version %d\n", version)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@ -198,19 +232,27 @@ func UpgradeDB(config *Config) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func LoadCloakSecret(db *buntdb.DB) (result string) {
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
result, _ = tx.Get(keyCloakSecret)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
func LoadCloakSecret(dstore datastore.Datastore) (result string, err error) {
|
||||
val, err := dstore.Get(datastore.TableMetadata, cloakSecretUUID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return string(val), nil
|
||||
}
|
||||
|
||||
func StoreCloakSecret(db *buntdb.DB, secret string) {
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(keyCloakSecret, secret, nil)
|
||||
return nil
|
||||
})
|
||||
func StoreCloakSecret(dstore datastore.Datastore, secret string) {
|
||||
// TODO error checking
|
||||
dstore.Set(datastore.TableMetadata, cloakSecretUUID, []byte(secret), time.Time{})
|
||||
}
|
||||
|
||||
func LoadVAPIDKeys(dstore datastore.Datastore) (*webpush.VAPIDKeys, error) {
|
||||
val, err := dstore.Get(datastore.TableMetadata, vapidKeysUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := new(webpush.VAPIDKeys)
|
||||
err = json.Unmarshal([]byte(val), result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
||||
@ -1008,6 +1050,210 @@ func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// #734: move the email address into the settings object,
|
||||
// giving people a way to change it
|
||||
func schemaChangeV20To21(config *Config, tx *buntdb.Tx) error {
|
||||
type accountSettingsv21 struct {
|
||||
AutoreplayLines *int
|
||||
NickEnforcement NickEnforcementMethod
|
||||
AllowBouncer MulticlientAllowedSetting
|
||||
ReplayJoins ReplayJoinsSetting
|
||||
AlwaysOn PersistentStatus
|
||||
AutoreplayMissed bool
|
||||
DMHistory HistoryStatus
|
||||
AutoAway PersistentStatus
|
||||
Email string
|
||||
}
|
||||
var accounts []string
|
||||
var emails []string
|
||||
callbackPrefix := "account.callback "
|
||||
tx.AscendGreaterOrEqual("", callbackPrefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, callbackPrefix) {
|
||||
return false
|
||||
}
|
||||
account := strings.TrimPrefix(key, callbackPrefix)
|
||||
if _, err := tx.Get("account.verified " + account); err != nil {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(value, "mailto:") {
|
||||
accounts = append(accounts, account)
|
||||
emails = append(emails, strings.TrimPrefix(value, "mailto:"))
|
||||
}
|
||||
return true
|
||||
})
|
||||
for i, account := range accounts {
|
||||
var settings accountSettingsv21
|
||||
email := emails[i]
|
||||
settingsKey := "account.settings " + account
|
||||
settingsStr, err := tx.Get(settingsKey)
|
||||
if err == nil && settingsStr != "" {
|
||||
json.Unmarshal([]byte(settingsStr), &settings)
|
||||
}
|
||||
settings.Email = email
|
||||
settingsBytes, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
log.Printf("couldn't marshal settings for %s: %v\n", account, err)
|
||||
} else {
|
||||
tx.Set(settingsKey, string(settingsBytes), nil)
|
||||
}
|
||||
tx.Delete(callbackPrefix + account)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// #1676: we used to have ReplayJoinsNever, now it's desupported
|
||||
func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
|
||||
type accountSettingsv22 struct {
|
||||
AutoreplayLines *int
|
||||
NickEnforcement NickEnforcementMethod
|
||||
AllowBouncer MulticlientAllowedSetting
|
||||
ReplayJoins ReplayJoinsSetting
|
||||
AlwaysOn PersistentStatus
|
||||
AutoreplayMissed bool
|
||||
DMHistory HistoryStatus
|
||||
AutoAway PersistentStatus
|
||||
Email string
|
||||
}
|
||||
|
||||
var accounts []string
|
||||
var serializedSettings []string
|
||||
settingsPrefix := "account.settings "
|
||||
tx.AscendGreaterOrEqual("", settingsPrefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, settingsPrefix) {
|
||||
return false
|
||||
}
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
account := strings.TrimPrefix(key, settingsPrefix)
|
||||
if _, err := tx.Get("account.verified " + account); err != nil {
|
||||
return true
|
||||
}
|
||||
var settings accountSettingsv22
|
||||
err := json.Unmarshal([]byte(value), &settings)
|
||||
if err != nil {
|
||||
log.Printf("error (v21-22) processing settings for %s: %v\n", account, err)
|
||||
return true
|
||||
}
|
||||
// if necessary, change ReplayJoinsNever (2) to ReplayJoinsCommandsOnly (0)
|
||||
if settings.ReplayJoins == ReplayJoinsSetting(2) {
|
||||
settings.ReplayJoins = ReplayJoinsSetting(0)
|
||||
if b, err := json.Marshal(settings); err == nil {
|
||||
accounts = append(accounts, account)
|
||||
serializedSettings = append(serializedSettings, string(b))
|
||||
} else {
|
||||
log.Printf("error (v21-22) processing settings for %s: %v\n", account, err)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for i, account := range accounts {
|
||||
tx.Set(settingsPrefix+account, serializedSettings[i], nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// first phase of document-oriented database refactor: channels
|
||||
func schemaChangeV22ToV23(config *Config, tx *buntdb.Tx) error {
|
||||
keyChannelExists := "channel.exists "
|
||||
var channelNames []string
|
||||
tx.AscendGreaterOrEqual("", keyChannelExists, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, keyChannelExists) {
|
||||
return false
|
||||
}
|
||||
channelNames = append(channelNames, strings.TrimPrefix(key, keyChannelExists))
|
||||
return true
|
||||
})
|
||||
for _, channelName := range channelNames {
|
||||
channel, err := loadLegacyChannel(tx, channelName)
|
||||
if err != nil {
|
||||
log.Printf("error loading legacy channel %s: %v", channelName, err)
|
||||
continue
|
||||
}
|
||||
channel.UUID = utils.GenerateUUIDv4()
|
||||
newKey := bunt.BuntKey(datastore.TableChannels, channel.UUID)
|
||||
j, err := json.Marshal(channel)
|
||||
if err != nil {
|
||||
log.Printf("error marshaling channel %s: %v", channelName, err)
|
||||
continue
|
||||
}
|
||||
tx.Set(newKey, string(j), nil)
|
||||
deleteLegacyChannel(tx, channelName)
|
||||
}
|
||||
|
||||
// purges
|
||||
keyChannelPurged := "channel.purged "
|
||||
var purgeKeys []string
|
||||
var channelPurges []ChannelPurgeRecord
|
||||
tx.AscendGreaterOrEqual("", keyChannelPurged, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, keyChannelPurged) {
|
||||
return false
|
||||
}
|
||||
purgeKeys = append(purgeKeys, key)
|
||||
cfname := strings.TrimPrefix(key, keyChannelPurged)
|
||||
var record ChannelPurgeRecord
|
||||
err := json.Unmarshal([]byte(value), &record)
|
||||
if err != nil {
|
||||
log.Printf("error unmarshaling channel purge for %s: %v", cfname, err)
|
||||
return true
|
||||
}
|
||||
record.NameCasefolded = cfname
|
||||
record.UUID = utils.GenerateUUIDv4()
|
||||
channelPurges = append(channelPurges, record)
|
||||
return true
|
||||
})
|
||||
for _, record := range channelPurges {
|
||||
newKey := bunt.BuntKey(datastore.TableChannelPurges, record.UUID)
|
||||
j, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
log.Printf("error marshaling channel purge %s: %v", record.NameCasefolded, err)
|
||||
continue
|
||||
}
|
||||
tx.Set(newKey, string(j), nil)
|
||||
}
|
||||
for _, purgeKey := range purgeKeys {
|
||||
tx.Delete(purgeKey)
|
||||
}
|
||||
|
||||
// clean up denormalized account-to-channels mapping
|
||||
keyAccountChannels := "account.channels "
|
||||
var accountToChannels []string
|
||||
tx.AscendGreaterOrEqual("", keyAccountChannels, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, keyAccountChannels) {
|
||||
return false
|
||||
}
|
||||
accountToChannels = append(accountToChannels, key)
|
||||
return true
|
||||
})
|
||||
for _, key := range accountToChannels {
|
||||
tx.Delete(key)
|
||||
}
|
||||
|
||||
// migrate cloak secret
|
||||
val, _ := tx.Get("crypto.cloak_secret")
|
||||
tx.Set(keyCloakSecret, val, nil)
|
||||
|
||||
// bump the legacy version key to mark the database as downgrade-incompatible
|
||||
tx.Set("db.version", "23", nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// webpush signing key
|
||||
func schemaChangeV23ToV24(config *Config, tx *buntdb.Tx) error {
|
||||
keys, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Set(keyVAPIDKeys, string(j), nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
||||
for _, change := range allChanges {
|
||||
if initialVersion == change.InitialVersion {
|
||||
@ -1113,4 +1359,24 @@ var allChanges = []SchemaChange{
|
||||
TargetVersion: 20,
|
||||
Changer: schemaChangeV19To20,
|
||||
},
|
||||
{
|
||||
InitialVersion: 20,
|
||||
TargetVersion: 21,
|
||||
Changer: schemaChangeV20To21,
|
||||
},
|
||||
{
|
||||
InitialVersion: 21,
|
||||
TargetVersion: 22,
|
||||
Changer: schemaChangeV21To22,
|
||||
},
|
||||
{
|
||||
InitialVersion: 22,
|
||||
TargetVersion: 23,
|
||||
Changer: schemaChangeV22ToV23,
|
||||
},
|
||||
{
|
||||
InitialVersion: 23,
|
||||
TargetVersion: 24,
|
||||
Changer: schemaChangeV23ToV24,
|
||||
},
|
||||
}
|
||||
|
||||
45
irc/datastore/datastore.go
Normal file
45
irc/datastore/datastore.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type Table uint16
|
||||
|
||||
// XXX these are persisted and must remain stable;
|
||||
// do not reorder, when deleting use _ to ensure that the deleted value is skipped
|
||||
const (
|
||||
TableMetadata Table = iota
|
||||
TableChannels
|
||||
TableChannelPurges
|
||||
)
|
||||
|
||||
type KV struct {
|
||||
UUID utils.UUID
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// A Datastore provides the following abstraction:
|
||||
// 1. Tables, each keyed on a UUID (the implementation is free to merge
|
||||
// the table name and the UUID into a single key as long as the rest of
|
||||
// the contract can be satisfied). Table names are [a-z0-9_]+
|
||||
// 2. The ability to efficiently enumerate all uuid-value pairs in a table
|
||||
// 3. Gets, sets, and deletes for individual (table, uuid) keys
|
||||
type Datastore interface {
|
||||
Backoff() time.Duration
|
||||
|
||||
GetAll(table Table) ([]KV, error)
|
||||
|
||||
// This is rarely used because it would typically lead to TOCTOU races
|
||||
Get(table Table, key utils.UUID) (value []byte, err error)
|
||||
|
||||
Set(table Table, key utils.UUID, value []byte, expiration time.Time) error
|
||||
|
||||
// Note that deleting a nonexistent key is not considered an error
|
||||
Delete(table Table, key utils.UUID) error
|
||||
}
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/flatip"
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
|
||||
@ -4,9 +4,18 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
dkim "github.com/toorop/go-dkim"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
dkim "github.com/emersion/go-msgauth/dkim"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -17,38 +26,77 @@ type DKIMConfig struct {
|
||||
Domain string
|
||||
Selector string
|
||||
KeyFile string `yaml:"key-file"`
|
||||
keyBytes []byte
|
||||
privKey crypto.Signer
|
||||
}
|
||||
|
||||
func (dkim *DKIMConfig) Enabled() bool {
|
||||
return dkim.Domain != ""
|
||||
}
|
||||
|
||||
func (dkim *DKIMConfig) Postprocess() (err error) {
|
||||
if dkim.Domain != "" {
|
||||
if dkim.Selector == "" || dkim.KeyFile == "" {
|
||||
return ErrMissingFields
|
||||
}
|
||||
dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dkim.Enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dkim.Selector == "" || dkim.KeyFile == "" {
|
||||
return ErrMissingFields
|
||||
}
|
||||
|
||||
keyBytes, err := os.ReadFile(dkim.KeyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not read DKIM key file: %w", err)
|
||||
}
|
||||
dkim.privKey, err = parseDKIMPrivKey(keyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not parse DKIM key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultOptions = dkim.SigOptions{
|
||||
Version: 1,
|
||||
Canonicalization: "relaxed/relaxed",
|
||||
Algo: "rsa-sha256",
|
||||
Headers: []string{"from", "to", "subject", "message-id", "date"},
|
||||
BodyLength: 0,
|
||||
QueryMethods: []string{"dns/txt"},
|
||||
AddSignatureTimestamp: true,
|
||||
SignatureExpireIn: 0,
|
||||
func parseDKIMPrivKey(input []byte) (crypto.Signer, error) {
|
||||
if len(input) == 0 {
|
||||
return nil, errors.New("DKIM private key is empty")
|
||||
}
|
||||
|
||||
// raw ed25519 private key format
|
||||
if len(input) == ed25519.PrivateKeySize {
|
||||
return ed25519.PrivateKey(input), nil
|
||||
}
|
||||
|
||||
d, _ := pem.Decode(input)
|
||||
if d == nil {
|
||||
return nil, errors.New("Invalid PEM data for DKIM private key")
|
||||
}
|
||||
|
||||
if rsaKey, err := x509.ParsePKCS1PrivateKey(d.Bytes); err == nil {
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
if k, err := x509.ParsePKCS8PrivateKey(d.Bytes); err == nil {
|
||||
switch key := k.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, nil
|
||||
case ed25519.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Unacceptable type for DKIM private key: %T", k)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("No acceptable format for DKIM private key")
|
||||
}
|
||||
|
||||
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
|
||||
options := defaultOptions
|
||||
options.PrivateKey = dkimConfig.keyBytes
|
||||
options.Domain = dkimConfig.Domain
|
||||
options.Selector = dkimConfig.Selector
|
||||
err = dkim.Sign(&message, options)
|
||||
return message, err
|
||||
options := dkim.SignOptions{
|
||||
Domain: dkimConfig.Domain,
|
||||
Selector: dkimConfig.Selector,
|
||||
Signer: dkimConfig.privKey,
|
||||
HeaderCanonicalization: dkim.CanonicalizationRelaxed,
|
||||
BodyCanonicalization: dkim.CanonicalizationRelaxed,
|
||||
}
|
||||
input := bytes.NewBuffer(message)
|
||||
output := bytes.NewBuffer(make([]byte, 0, len(message)+1024))
|
||||
err = dkim.Sign(output, input, &options)
|
||||
return output.Bytes(), err
|
||||
}
|
||||
|
||||
@ -4,42 +4,131 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/smtp"
|
||||
"github.com/ergochat/ergo/irc/custime"
|
||||
"github.com/ergochat/ergo/irc/smtp"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBlacklistedAddress = errors.New("Email address is blacklisted")
|
||||
ErrInvalidAddress = errors.New("Email address is blacklisted")
|
||||
ErrInvalidAddress = errors.New("Email address is invalid")
|
||||
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
|
||||
)
|
||||
|
||||
type BlacklistSyntax uint
|
||||
|
||||
const (
|
||||
BlacklistSyntaxGlob BlacklistSyntax = iota
|
||||
BlacklistSyntaxRegexp
|
||||
)
|
||||
|
||||
func blacklistSyntaxFromString(status string) (BlacklistSyntax, error) {
|
||||
switch strings.ToLower(status) {
|
||||
case "glob", "":
|
||||
return BlacklistSyntaxGlob, nil
|
||||
case "re", "regex", "regexp":
|
||||
return BlacklistSyntaxRegexp, nil
|
||||
default:
|
||||
return BlacklistSyntaxRegexp, fmt.Errorf("Unknown blacklist syntax type `%s`", status)
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BlacklistSyntax) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var orig string
|
||||
var err error
|
||||
if err = unmarshal(&orig); err != nil {
|
||||
return err
|
||||
}
|
||||
if result, err := blacklistSyntaxFromString(orig); err == nil {
|
||||
*bs = result
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type MTAConfig struct {
|
||||
Server string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Server string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
ImplicitTLS bool `yaml:"implicit-tls"`
|
||||
}
|
||||
|
||||
type MailtoConfig struct {
|
||||
// legacy config format assumed the use of an MTA/smarthost,
|
||||
// so server, port, etc. appear directly at top level
|
||||
// XXX: see https://github.com/go-yaml/yaml/issues/63
|
||||
MTAConfig `yaml:",inline"`
|
||||
Enabled bool
|
||||
Sender string
|
||||
HeloDomain string `yaml:"helo-domain"`
|
||||
RequireTLS bool `yaml:"require-tls"`
|
||||
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
||||
DKIM DKIMConfig
|
||||
MTAReal MTAConfig `yaml:"mta"`
|
||||
BlacklistRegexes []string `yaml:"blacklist-regexes"`
|
||||
blacklistRegexes []*regexp.Regexp
|
||||
MTAConfig `yaml:",inline"`
|
||||
Enabled bool
|
||||
Sender string
|
||||
HeloDomain string `yaml:"helo-domain"`
|
||||
RequireTLS bool `yaml:"require-tls"`
|
||||
Protocol string `yaml:"protocol"`
|
||||
LocalAddress string `yaml:"local-address"`
|
||||
localAddress net.Addr
|
||||
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
||||
DKIM DKIMConfig
|
||||
MTAReal MTAConfig `yaml:"mta"`
|
||||
AddressBlacklist []string `yaml:"address-blacklist"`
|
||||
AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"`
|
||||
AddressBlacklistFile string `yaml:"address-blacklist-file"`
|
||||
blacklistRegexes []*regexp.Regexp
|
||||
Timeout time.Duration
|
||||
PasswordReset struct {
|
||||
Enabled bool
|
||||
Cooldown custime.Duration
|
||||
Timeout custime.Duration
|
||||
} `yaml:"password-reset"`
|
||||
}
|
||||
|
||||
func (config *MailtoConfig) compileBlacklistEntry(source string) (re *regexp.Regexp, err error) {
|
||||
if config.AddressBlacklistSyntax == BlacklistSyntaxGlob {
|
||||
return utils.CompileGlob(source, false)
|
||||
} else {
|
||||
return regexp.Compile(fmt.Sprintf("^%s$", source))
|
||||
}
|
||||
}
|
||||
|
||||
func (config *MailtoConfig) processBlacklistFile(filename string) (result []*regexp.Regexp, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
reader := bufio.NewReader(f)
|
||||
lineNo := 0
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
lineNo++
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && line[0] != '#' {
|
||||
if compiled, compileErr := config.compileBlacklistEntry(line); compileErr == nil {
|
||||
result = append(result, compiled)
|
||||
} else {
|
||||
return result, fmt.Errorf("Failed to compile line %d of blacklist-regex-file `%s`: %w", lineNo, line, compileErr)
|
||||
}
|
||||
}
|
||||
switch err {
|
||||
case io.EOF:
|
||||
return result, nil
|
||||
case nil:
|
||||
continue
|
||||
default:
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||
@ -57,12 +146,39 @@ func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||
config.HeloDomain = heloDomain
|
||||
}
|
||||
|
||||
for _, reg := range config.BlacklistRegexes {
|
||||
compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
|
||||
if config.AddressBlacklistFile != "" {
|
||||
config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
||||
} else if len(config.AddressBlacklist) != 0 {
|
||||
config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
|
||||
for _, reg := range config.AddressBlacklist {
|
||||
compiled, err := config.compileBlacklistEntry(reg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
||||
}
|
||||
}
|
||||
|
||||
config.Protocol = strings.ToLower(config.Protocol)
|
||||
if config.Protocol == "" {
|
||||
config.Protocol = "tcp"
|
||||
}
|
||||
if !(config.Protocol == "tcp" || config.Protocol == "tcp4" || config.Protocol == "tcp6") {
|
||||
return fmt.Errorf("Invalid protocol for email sending: `%s`", config.Protocol)
|
||||
}
|
||||
|
||||
if config.LocalAddress != "" {
|
||||
ipAddr := net.ParseIP(config.LocalAddress)
|
||||
if ipAddr == nil {
|
||||
return fmt.Errorf("Could not parse local-address for email sending: `%s`", config.LocalAddress)
|
||||
}
|
||||
config.localAddress = &net.TCPAddr{
|
||||
IP: ipAddr,
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if config.MTAConfig.Server != "" {
|
||||
@ -73,6 +189,11 @@ func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||
return config.DKIM.Postprocess()
|
||||
}
|
||||
|
||||
// are we sending email directly, as opposed to deferring to an MTA?
|
||||
func (config *MailtoConfig) DirectSendingEnabled() bool {
|
||||
return config.MTAReal.Server == ""
|
||||
}
|
||||
|
||||
// get the preferred MX record hostname, "" on error
|
||||
func lookupMX(domain string) (server string) {
|
||||
var minPref uint16
|
||||
@ -88,14 +209,31 @@ func lookupMX(domain string) (server string) {
|
||||
return
|
||||
}
|
||||
|
||||
func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
|
||||
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
|
||||
fmt.Fprintf(&message, "To: %s\r\n", recipient)
|
||||
dkimDomain := config.DKIM.Domain
|
||||
if dkimDomain != "" {
|
||||
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
|
||||
} else {
|
||||
// #2108: send Message-ID even if dkim is not enabled
|
||||
fmt.Fprintf(&message, "Message-ID: <%s-%s>\r\n", utils.GenerateSecretKey(), config.Sender)
|
||||
}
|
||||
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
|
||||
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
|
||||
message.WriteString("\r\n") // blank line: end headers, begin message body
|
||||
return message
|
||||
}
|
||||
|
||||
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||
recipientLower := strings.ToLower(recipient)
|
||||
for _, reg := range config.blacklistRegexes {
|
||||
if reg.MatchString(recipient) {
|
||||
if reg.MatchString(recipientLower) {
|
||||
return ErrBlacklistedAddress
|
||||
}
|
||||
}
|
||||
|
||||
if config.DKIM.Domain != "" {
|
||||
if config.DKIM.Enabled() {
|
||||
msg, err = DKIMSign(msg, config.DKIM)
|
||||
if err != nil {
|
||||
return
|
||||
@ -104,11 +242,13 @@ func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||
|
||||
var addr string
|
||||
var auth smtp.Auth
|
||||
if config.MTAReal.Server != "" {
|
||||
var implicitTLS bool
|
||||
if !config.DirectSendingEnabled() {
|
||||
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
|
||||
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
|
||||
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
|
||||
}
|
||||
implicitTLS = config.MTAReal.ImplicitTLS
|
||||
} else {
|
||||
idx := strings.IndexByte(recipient, '@')
|
||||
if idx == -1 {
|
||||
@ -121,5 +261,8 @@ func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||
addr = fmt.Sprintf("%s:smtp", mx)
|
||||
}
|
||||
|
||||
return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS)
|
||||
return smtp.SendMail(
|
||||
addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
|
||||
config.RequireTLS, implicitTLS, config.Protocol, config.localAddress, config.Timeout,
|
||||
)
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// Runtime Errors
|
||||
@ -33,8 +33,8 @@ var (
|
||||
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
||||
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
|
||||
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
|
||||
errAuthRequired = errors.New("You must be logged into an account to do this")
|
||||
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`)
|
||||
errCallbackFailed = errors.New("Account verification could not be sent")
|
||||
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
|
||||
@ -52,6 +52,7 @@ var (
|
||||
errNoExistingBan = errors.New("Ban does not exist")
|
||||
errNoSuchChannel = errors.New(`No such channel`)
|
||||
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||
errChannelPurgedAlready = errors.New(`This channel was already purged and cannot be purged again`)
|
||||
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
@ -64,6 +65,7 @@ var (
|
||||
errCASFailed = errors.New("Compare-and-swap update of database value failed")
|
||||
errEmptyCredentials = errors.New("No more credentials are approved")
|
||||
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
|
||||
errNoSCRAMCredentials = errors.New("SCRAM credentials are not initialized for this account; consult the user guide")
|
||||
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
|
||||
errTimedOut = errors.New("Operation timed out")
|
||||
errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
|
||||
@ -74,6 +76,8 @@ var (
|
||||
errRegisteredOnly = errors.New("Cannot join registered-only channel without an account")
|
||||
errValidEmailRequired = errors.New("A valid email address is required for account registration")
|
||||
errInvalidAccountRename = errors.New("Account renames can only change the casefolding of the account name")
|
||||
errNameReserved = errors.New(`Name reserved due to a prior registration`)
|
||||
errInvalidBearerTokenType = errors.New("invalid bearer token type")
|
||||
)
|
||||
|
||||
// String Errors
|
||||
@ -96,5 +100,5 @@ type ThrottleError struct {
|
||||
}
|
||||
|
||||
func (te *ThrottleError) Error() string {
|
||||
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration)
|
||||
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration.Round(time.Millisecond))
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -36,6 +37,10 @@ type Fakelag struct {
|
||||
|
||||
func (fl *Fakelag) Initialize(config FakelagConfig) {
|
||||
fl.config = config
|
||||
// XXX don't share mutable member CommandBudgets:
|
||||
if config.CommandBudgets != nil {
|
||||
fl.config.CommandBudgets = maps.Clone(config.CommandBudgets)
|
||||
}
|
||||
fl.nowFunc = time.Now
|
||||
fl.sleepFunc = time.Sleep
|
||||
fl.state = FakelagBursting
|
||||
@ -58,11 +63,16 @@ func (fl *Fakelag) Unsuspend() {
|
||||
}
|
||||
|
||||
// register a new command, sleep if necessary to delay it
|
||||
func (fl *Fakelag) Touch() {
|
||||
func (fl *Fakelag) Touch(command string) {
|
||||
if !fl.config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 {
|
||||
fl.config.CommandBudgets[command] = budget - 1
|
||||
return
|
||||
}
|
||||
|
||||
now := fl.nowFunc()
|
||||
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
|
||||
elapsed := now.Sub(fl.lastTouch)
|
||||
|
||||
@ -60,7 +60,7 @@ func TestFakelag(t *testing.T) {
|
||||
window, _ := time.ParseDuration("1s")
|
||||
fl, mt := newFakelagForTesting(window, 3, 2, window)
|
||||
|
||||
fl.Touch()
|
||||
fl.Touch("")
|
||||
slept, _ := mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
@ -69,7 +69,7 @@ func TestFakelag(t *testing.T) {
|
||||
interval, _ := time.ParseDuration("100ms")
|
||||
for i := 0; i < 2; i++ {
|
||||
mt.pause(interval)
|
||||
fl.Touch()
|
||||
fl.Touch("")
|
||||
slept, _ := mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
@ -77,7 +77,7 @@ func TestFakelag(t *testing.T) {
|
||||
}
|
||||
|
||||
mt.pause(interval)
|
||||
fl.Touch()
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should be throttled")
|
||||
}
|
||||
@ -91,7 +91,7 @@ func TestFakelag(t *testing.T) {
|
||||
}
|
||||
|
||||
// send another message without a pause; we should have to sleep for 500 msec
|
||||
fl.Touch()
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should be throttled")
|
||||
}
|
||||
@ -102,7 +102,7 @@ func TestFakelag(t *testing.T) {
|
||||
}
|
||||
|
||||
mt.pause(interval * 6)
|
||||
fl.Touch()
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should still be throttled")
|
||||
}
|
||||
@ -112,7 +112,7 @@ func TestFakelag(t *testing.T) {
|
||||
}
|
||||
|
||||
mt.pause(window * 2)
|
||||
fl.Touch()
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagBursting {
|
||||
t.Fatalf("should be bursting again")
|
||||
}
|
||||
@ -125,31 +125,31 @@ func TestFakelag(t *testing.T) {
|
||||
func TestSuspend(t *testing.T) {
|
||||
window, _ := time.ParseDuration("1s")
|
||||
fl, _ := newFakelagForTesting(window, 3, 2, window)
|
||||
assertEqual(fl.config.Enabled, true, t)
|
||||
assertEqual(fl.config.Enabled, true)
|
||||
|
||||
// suspend idempotently disables
|
||||
fl.Suspend()
|
||||
assertEqual(fl.config.Enabled, false, t)
|
||||
assertEqual(fl.config.Enabled, false)
|
||||
fl.Suspend()
|
||||
assertEqual(fl.config.Enabled, false, t)
|
||||
assertEqual(fl.config.Enabled, false)
|
||||
// unsuspend idempotently enables
|
||||
fl.Unsuspend()
|
||||
assertEqual(fl.config.Enabled, true, t)
|
||||
assertEqual(fl.config.Enabled, true)
|
||||
fl.Unsuspend()
|
||||
assertEqual(fl.config.Enabled, true, t)
|
||||
assertEqual(fl.config.Enabled, true)
|
||||
fl.Suspend()
|
||||
assertEqual(fl.config.Enabled, false, t)
|
||||
assertEqual(fl.config.Enabled, false)
|
||||
|
||||
fl2, _ := newFakelagForTesting(window, 3, 2, window)
|
||||
fl2.config.Enabled = false
|
||||
|
||||
// if we were never enabled, suspend and unsuspend are both no-ops
|
||||
fl2.Suspend()
|
||||
assertEqual(fl2.config.Enabled, false, t)
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
fl2.Suspend()
|
||||
assertEqual(fl2.config.Enabled, false, t)
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
fl2.Unsuspend()
|
||||
assertEqual(fl2.config.Enabled, false, t)
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
fl2.Unsuspend()
|
||||
assertEqual(fl2.config.Enabled, false, t)
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
}
|
||||
|
||||
@ -155,6 +155,14 @@ func (cidr IPNet) Contains(ip IP) bool {
|
||||
return cidr.IP == maskedIP
|
||||
}
|
||||
|
||||
func (cidr IPNet) Size() (ones, bits int) {
|
||||
if cidr.IP.IsIPv4() {
|
||||
return int(cidr.PrefixLen) - 96, 32
|
||||
} else {
|
||||
return int(cidr.PrefixLen), 128
|
||||
}
|
||||
}
|
||||
|
||||
// FromNetIPnet converts a net.IPNet into an IPNet.
|
||||
func FromNetIPNet(network net.IPNet) (result IPNet) {
|
||||
ones, _ := network.Mask.Size()
|
||||
|
||||
@ -2,8 +2,10 @@ package flatip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@ -86,6 +88,38 @@ func doMaskingTest(ip net.IP, t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(found, expected interface{}) {
|
||||
if !reflect.DeepEqual(found, expected) {
|
||||
panic(fmt.Sprintf("expected %#v, found %#v", expected, found))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
_, net, err := ParseCIDR("8.8.8.8/24")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ones, bits := net.Size()
|
||||
assertEqual(ones, 24)
|
||||
assertEqual(bits, 32)
|
||||
|
||||
_, net, err = ParseCIDR("2001::0db8/64")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ones, bits = net.Size()
|
||||
assertEqual(ones, 64)
|
||||
assertEqual(bits, 128)
|
||||
|
||||
_, net, err = ParseCIDR("2001::0db8/96")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ones, bits = net.Size()
|
||||
assertEqual(ones, 96)
|
||||
assertEqual(bits, 128)
|
||||
}
|
||||
|
||||
func TestMasking(t *testing.T) {
|
||||
for _, ipstr := range testIPStrs {
|
||||
doMaskingTest(easyParseIP(ipstr), t)
|
||||
|
||||
24
irc/flock/flock.go
Normal file
24
irc/flock/flock.go
Normal file
@ -0,0 +1,24 @@
|
||||
//go:build !(plan9 || solaris)
|
||||
|
||||
package flock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
)
|
||||
|
||||
var (
|
||||
CouldntAcquire = errors.New("Couldn't acquire flock (is another Ergo running?)")
|
||||
)
|
||||
|
||||
func TryAcquireFlock(path string) (fl Flocker, err error) {
|
||||
f := flock.New(path)
|
||||
success, err := f.TryLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !success {
|
||||
return nil, CouldntAcquire
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
14
irc/flock/flock_iface.go
Normal file
14
irc/flock/flock_iface.go
Normal file
@ -0,0 +1,14 @@
|
||||
package flock
|
||||
|
||||
// documentation for github.com/gofrs/flock incorrectly claims that
|
||||
// Flock implements sync.Locker; it does not because the Unlock method
|
||||
// has a return type (err).
|
||||
type Flocker interface {
|
||||
Unlock() error
|
||||
}
|
||||
|
||||
type noopFlocker struct{}
|
||||
|
||||
func (n *noopFlocker) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
7
irc/flock/flock_unsupported.go
Normal file
7
irc/flock/flock_unsupported.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build plan9 || solaris
|
||||
|
||||
package flock
|
||||
|
||||
func TryAcquireFlock(path string) (fl Flocker, err error) {
|
||||
return &noopFlocker{}, nil
|
||||
}
|
||||
@ -9,9 +9,9 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/oragono/oragono/irc/flatip"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -32,6 +32,7 @@ type webircConfig struct {
|
||||
Fingerprint *string // legacy name for certfp, #1050
|
||||
Certfp string
|
||||
Hosts []string
|
||||
AcceptHostname bool `yaml:"accept-hostname"`
|
||||
allowedNets []net.IPNet
|
||||
}
|
||||
|
||||
@ -91,7 +92,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls boo
|
||||
client.server.connectionLimiter.RemoveClient(flatip.FromNetIP(session.realIP))
|
||||
|
||||
// given IP is sane! override the client's current IP
|
||||
client.server.logger.Info("connect-ip", "Accepted proxy IP for client", proxiedIP.String())
|
||||
client.server.logger.Info("connect-ip", session.connID, "Accepted proxy IP for client", proxiedIP.String())
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
663
irc/getters.go
663
irc/getters.go
@ -4,26 +4,22 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"slices"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/connection_limits"
|
||||
"github.com/ergochat/ergo/irc/languages"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/webpush"
|
||||
)
|
||||
|
||||
func (server *Server) Config() (config *Config) {
|
||||
return (*Config)(atomic.LoadPointer(&server.config))
|
||||
}
|
||||
|
||||
func (server *Server) SetConfig(config *Config) {
|
||||
atomic.StorePointer(&server.config, unsafe.Pointer(config))
|
||||
}
|
||||
|
||||
func (server *Server) ChannelRegistrationEnabled() bool {
|
||||
return server.Config().Channels.Registration.Enabled
|
||||
return server.config.Load()
|
||||
}
|
||||
|
||||
func (server *Server) GetOperator(name string) (oper *Oper) {
|
||||
@ -39,11 +35,11 @@ func (server *Server) Languages() (lm *languages.Manager) {
|
||||
}
|
||||
|
||||
func (server *Server) Defcon() uint32 {
|
||||
return atomic.LoadUint32(&server.defcon)
|
||||
return server.defcon.Load()
|
||||
}
|
||||
|
||||
func (server *Server) SetDefcon(defcon uint32) {
|
||||
atomic.StoreUint32(&server.defcon, defcon)
|
||||
server.defcon.Store(defcon)
|
||||
}
|
||||
|
||||
func (client *Client) Sessions() (sessions []*Session) {
|
||||
@ -53,18 +49,6 @@ func (client *Client) Sessions() (sessions []*Session) {
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) GetSessionByResumeID(resumeID string) (result *Session) {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
||||
for _, session := range client.sessions {
|
||||
if session.resumeID == resumeID {
|
||||
return session
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type SessionData struct {
|
||||
ctime time.Time
|
||||
atime time.Time
|
||||
@ -73,7 +57,9 @@ type SessionData struct {
|
||||
certfp string
|
||||
deviceID string
|
||||
connInfo string
|
||||
connID string
|
||||
sessionID int64
|
||||
caps []string
|
||||
}
|
||||
|
||||
func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (data []SessionData, currentIndex int) {
|
||||
@ -92,6 +78,7 @@ func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (da
|
||||
hostname: session.rawHostname,
|
||||
certfp: session.certfp,
|
||||
deviceID: session.deviceID,
|
||||
connID: session.connID,
|
||||
sessionID: session.sessionID,
|
||||
}
|
||||
if session.proxiedIP != nil {
|
||||
@ -102,11 +89,13 @@ func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (da
|
||||
if hasPrivs {
|
||||
data[i].connInfo = utils.DescribeConn(session.socket.conn.UnderlyingConn().Conn)
|
||||
}
|
||||
data[i].caps = session.capabilities.Strings(caps.Cap302, nil, 300)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSeen time.Time, back bool) {
|
||||
func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSeen time.Time, wasAway, nowAway string) {
|
||||
config := client.server.Config()
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
@ -123,16 +112,25 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in
|
||||
newSessions[len(newSessions)-1] = session
|
||||
if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
|
||||
lastSeen = client.lastSeen[session.deviceID]
|
||||
client.setLastSeen(time.Now().UTC(), session.deviceID)
|
||||
}
|
||||
client.setLastSeen(time.Now().UTC(), session.deviceID)
|
||||
client.sessions = newSessions
|
||||
if client.autoAway {
|
||||
back = true
|
||||
client.autoAway = false
|
||||
client.away = false
|
||||
client.awayMessage = ""
|
||||
wasAway = client.awayMessage
|
||||
if client.autoAwayEnabledNoMutex(config) {
|
||||
client.setAutoAwayNoMutex(config)
|
||||
} else {
|
||||
if session.awayMessage != "" && session.awayMessage != "*" {
|
||||
// set the away message
|
||||
client.awayMessage = session.awayMessage
|
||||
} else if session.awayMessage == "" && !session.awayAt.IsZero() {
|
||||
// weird edge case: explicit `AWAY` or `AWAY :` during pre-registration makes the client back
|
||||
client.awayMessage = ""
|
||||
}
|
||||
// else: the client sent no AWAY command at all, no-op
|
||||
// or: the client sent `AWAY *`, which should not modify the publicly visible away state
|
||||
}
|
||||
return true, len(client.sessions), lastSeen, back
|
||||
nowAway = client.awayMessage
|
||||
return true, len(client.sessions), lastSeen, wasAway, nowAway
|
||||
}
|
||||
|
||||
func (client *Client) removeSession(session *Session) (success bool, length int) {
|
||||
@ -152,10 +150,15 @@ func (client *Client) removeSession(session *Session) (success bool, length int)
|
||||
return
|
||||
}
|
||||
|
||||
func (session *Session) SetResumeID(resumeID string) {
|
||||
session.client.stateMutex.Lock()
|
||||
session.resumeID = resumeID
|
||||
session.client.stateMutex.Unlock()
|
||||
// #1650: show an arbitrarily chosen session IP and hostname in RPL_WHOISACTUALLY
|
||||
func (client *Client) getWhoisActually() (ip net.IP, hostname string) {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
||||
for _, session := range client.sessions {
|
||||
return session.IP(), session.rawHostname
|
||||
}
|
||||
return utils.IPv4LoopbackAddress, client.server.name
|
||||
}
|
||||
|
||||
func (client *Client) Nick() string {
|
||||
@ -196,20 +199,67 @@ func (client *Client) Hostname() string {
|
||||
|
||||
func (client *Client) Away() (result bool, message string) {
|
||||
client.stateMutex.Lock()
|
||||
result, message = client.away, client.awayMessage
|
||||
message = client.awayMessage
|
||||
client.stateMutex.Unlock()
|
||||
result = client.awayMessage != ""
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) SetAway(away bool, awayMessage string) (changed bool) {
|
||||
func (session *Session) SetAway(awayMessage string) (wasAway, nowAway string) {
|
||||
client := session.client
|
||||
config := client.server.Config()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
changed = away != client.away
|
||||
client.away = away
|
||||
client.awayMessage = awayMessage
|
||||
client.stateMutex.Unlock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
session.awayMessage = awayMessage
|
||||
session.awayAt = time.Now().UTC()
|
||||
|
||||
wasAway = client.awayMessage
|
||||
if client.autoAwayEnabledNoMutex(config) {
|
||||
client.setAutoAwayNoMutex(config)
|
||||
} else if awayMessage != "*" {
|
||||
client.awayMessage = awayMessage
|
||||
} // else: `AWAY *`, should not modify publicly visible away state
|
||||
nowAway = client.awayMessage
|
||||
return
|
||||
}
|
||||
|
||||
func (session *Session) ConnID() string {
|
||||
if session == nil {
|
||||
return "*"
|
||||
}
|
||||
return session.connID
|
||||
}
|
||||
|
||||
func (client *Client) autoAwayEnabledNoMutex(config *Config) bool {
|
||||
return client.registered && client.alwaysOn &&
|
||||
persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway)
|
||||
}
|
||||
|
||||
func (client *Client) setAutoAwayNoMutex(config *Config) {
|
||||
// aggregate the away statuses of the individual sessions:
|
||||
var globalAwayState string
|
||||
var awaySetAt time.Time
|
||||
for _, cSession := range client.sessions {
|
||||
if cSession.awayMessage == "" {
|
||||
// a session is active, we are not auto-away
|
||||
client.awayMessage = ""
|
||||
return
|
||||
} else if cSession.awayAt.After(awaySetAt) && cSession.awayMessage != "*" {
|
||||
// choose the latest valid away message from any session
|
||||
globalAwayState = cSession.awayMessage
|
||||
awaySetAt = cSession.awayAt
|
||||
}
|
||||
}
|
||||
if awaySetAt.IsZero() {
|
||||
// no sessions, enable auto-away
|
||||
client.awayMessage = config.languageManager.Translate(client.languages, `User is currently disconnected`)
|
||||
} else {
|
||||
client.awayMessage = globalAwayState
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) AlwaysOn() (alwaysOn bool) {
|
||||
client.stateMutex.RLock()
|
||||
alwaysOn = client.registered && client.alwaysOn
|
||||
@ -226,18 +276,6 @@ func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton strin
|
||||
return client.nickCasefolded, client.skeleton
|
||||
}
|
||||
|
||||
func (client *Client) ResumeID() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.resumeID
|
||||
}
|
||||
|
||||
func (client *Client) SetResumeID(id string) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
client.resumeID = id
|
||||
}
|
||||
|
||||
func (client *Client) Oper() *Oper {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
@ -269,12 +307,6 @@ func (client *Client) AwayMessage() (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) SetAwayMessage(message string) {
|
||||
client.stateMutex.Lock()
|
||||
client.awayMessage = message
|
||||
client.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (client *Client) Account() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
@ -433,6 +465,7 @@ func (client *Client) detailsNoMutex() (result ClientDetails) {
|
||||
result.username = client.username
|
||||
result.hostname = client.hostname
|
||||
result.realname = client.realname
|
||||
result.ip = client.getIPNoMutex()
|
||||
result.nickMask = client.nickMaskString
|
||||
result.nickMaskCasefolded = client.nickMaskCasefolded
|
||||
result.account = client.account
|
||||
@ -465,6 +498,9 @@ func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config, ignoreRegis
|
||||
if !((client.registered || ignoreRegistration) && client.alwaysOn) {
|
||||
return false
|
||||
}
|
||||
if len(client.lastSeen) == 0 {
|
||||
return true // #2252: do not precreate the client if it was never logged into at all
|
||||
}
|
||||
deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
|
||||
if deadline == 0 {
|
||||
return false
|
||||
@ -478,6 +514,226 @@ func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config, ignoreRegis
|
||||
return true
|
||||
}
|
||||
|
||||
func (client *Client) GetReadMarker(cfname string) (result string) {
|
||||
client.stateMutex.RLock()
|
||||
t, ok := client.readMarkers[cfname]
|
||||
client.stateMutex.RUnlock()
|
||||
if ok {
|
||||
return fmt.Sprintf("timestamp=%s", t.Format(utils.IRCv3TimestampFormat))
|
||||
}
|
||||
return "*"
|
||||
}
|
||||
|
||||
func (client *Client) getMarkreadTime(cfname string) (timestamp time.Time, ok bool) {
|
||||
client.stateMutex.RLock()
|
||||
timestamp, ok = client.readMarkers[cfname]
|
||||
client.stateMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) copyReadMarkers() (result map[string]time.Time) {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return maps.Clone(client.readMarkers)
|
||||
}
|
||||
|
||||
func (client *Client) SetReadMarker(cfname string, now time.Time) (result time.Time) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
if client.readMarkers == nil {
|
||||
client.readMarkers = make(map[string]time.Time)
|
||||
}
|
||||
result = updateLRUMap(client.readMarkers, cfname, now, maxReadMarkers)
|
||||
client.dirtyTimestamps = true
|
||||
return
|
||||
}
|
||||
|
||||
func updateLRUMap(lru map[string]time.Time, key string, val time.Time, maxItems int) (result time.Time) {
|
||||
if currentVal := lru[key]; currentVal.After(val) {
|
||||
return currentVal
|
||||
}
|
||||
|
||||
lru[key] = val
|
||||
// evict the least-recently-used entry if necessary
|
||||
if maxItems < len(lru) {
|
||||
var minKey string
|
||||
var minVal time.Time
|
||||
for key, val := range lru {
|
||||
if minVal.IsZero() || val.Before(minVal) {
|
||||
minKey, minVal = key, val
|
||||
}
|
||||
}
|
||||
delete(lru, minKey)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (client *Client) addClearablePushMessage(cftarget string, messageTime time.Time) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
if client.clearablePushMessages == nil {
|
||||
client.clearablePushMessages = make(map[string]time.Time)
|
||||
}
|
||||
updateLRUMap(client.clearablePushMessages, cftarget, messageTime, maxReadMarkers)
|
||||
}
|
||||
|
||||
func (client *Client) clearClearablePushMessage(cftarget string, readTimestamp time.Time) (ok bool) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
pushMessageTime, ok := client.clearablePushMessages[cftarget]
|
||||
if ok && utils.ReadMarkerLessThanOrEqual(pushMessageTime, readTimestamp) {
|
||||
delete(client.clearablePushMessages, cftarget)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (client *Client) shouldFlushTimestamps() (result bool) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
result = client.dirtyTimestamps && client.registered && client.alwaysOn
|
||||
client.dirtyTimestamps = false
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) setKlined() {
|
||||
client.stateMutex.Lock()
|
||||
client.isKlined = true
|
||||
client.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (client *Client) refreshPushSubscription(endpoint string, keys webpush.Keys) bool {
|
||||
// do not mark dirty --- defer the write to periodic maintenance
|
||||
now := time.Now().UTC()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
sub, ok := client.pushSubscriptions[endpoint]
|
||||
if ok && sub.Keys.Equal(keys) {
|
||||
sub.LastRefresh = now
|
||||
return true
|
||||
}
|
||||
return false // subscription doesn't exist, we need to send a test message
|
||||
}
|
||||
|
||||
func (client *Client) addPushSubscription(endpoint string, keys webpush.Keys) error {
|
||||
changed := false
|
||||
|
||||
defer func() {
|
||||
if changed {
|
||||
client.markDirty(IncludeAllAttrs)
|
||||
}
|
||||
}()
|
||||
|
||||
config := client.server.Config()
|
||||
now := time.Now().UTC()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
if client.pushSubscriptions == nil {
|
||||
client.pushSubscriptions = make(map[string]*pushSubscription)
|
||||
}
|
||||
|
||||
sub, ok := client.pushSubscriptions[endpoint]
|
||||
if ok {
|
||||
changed = !sub.Keys.Equal(keys)
|
||||
sub.Keys = keys
|
||||
sub.LastRefresh = now
|
||||
} else {
|
||||
if len(client.pushSubscriptions) >= config.WebPush.MaxSubscriptions {
|
||||
return errLimitExceeded
|
||||
}
|
||||
changed = true
|
||||
sub = newPushSubscription(storedPushSubscription{
|
||||
Endpoint: endpoint,
|
||||
Keys: keys,
|
||||
LastRefresh: now,
|
||||
LastSuccess: now, // assume we just sent a successful message to confirm the sub
|
||||
})
|
||||
client.pushSubscriptions[endpoint] = sub
|
||||
}
|
||||
|
||||
if changed {
|
||||
client.rebuildPushSubscriptionCache()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) hasPushSubscriptions() bool {
|
||||
return client.pushSubscriptionsExist.Load() != 0
|
||||
}
|
||||
|
||||
func (client *Client) getPushSubscriptions(refresh bool) []storedPushSubscription {
|
||||
if refresh {
|
||||
func() {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
client.rebuildPushSubscriptionCache()
|
||||
}()
|
||||
}
|
||||
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.cachedPushSubscriptions
|
||||
}
|
||||
|
||||
func (client *Client) rebuildPushSubscriptionCache() {
|
||||
// must hold write lock
|
||||
if len(client.pushSubscriptions) == 0 {
|
||||
client.cachedPushSubscriptions = nil
|
||||
client.pushSubscriptionsExist.Store(0)
|
||||
return
|
||||
}
|
||||
|
||||
client.cachedPushSubscriptions = make([]storedPushSubscription, 0, len(client.pushSubscriptions))
|
||||
for _, subscription := range client.pushSubscriptions {
|
||||
client.cachedPushSubscriptions = append(client.cachedPushSubscriptions, subscription.storedPushSubscription)
|
||||
}
|
||||
client.pushSubscriptionsExist.Store(1)
|
||||
}
|
||||
|
||||
func (client *Client) deletePushSubscription(endpoint string, writeback bool) (changed bool) {
|
||||
defer func() {
|
||||
if writeback && changed {
|
||||
client.markDirty(IncludeAllAttrs)
|
||||
}
|
||||
}()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
_, ok := client.pushSubscriptions[endpoint]
|
||||
if ok {
|
||||
changed = true
|
||||
delete(client.pushSubscriptions, endpoint)
|
||||
client.rebuildPushSubscriptionCache()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) recordPush(endpoint string, success bool) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
subscription, ok := client.pushSubscriptions[endpoint]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if success {
|
||||
subscription.LastSuccess = now
|
||||
}
|
||||
// TODO we may want to track failures in some way in the future
|
||||
}
|
||||
|
||||
func (channel *Channel) Name() string {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
@ -528,9 +784,11 @@ func (channel *Channel) Founder() string {
|
||||
|
||||
func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
|
||||
channel.stateMutex.RLock()
|
||||
clientModes := channel.members[client].modes
|
||||
channel.stateMutex.RUnlock()
|
||||
return clientModes.HighestChannelUserMode()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
if clientData, ok := channel.members[client]; ok {
|
||||
return clientData.modes.HighestChannelUserMode()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (channel *Channel) Settings() (result ChannelSettings) {
|
||||
@ -541,10 +799,12 @@ func (channel *Channel) Settings() (result ChannelSettings) {
|
||||
}
|
||||
|
||||
func (channel *Channel) SetSettings(settings ChannelSettings) {
|
||||
defer channel.MarkDirty(IncludeSettings)
|
||||
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
channel.settings = settings
|
||||
channel.stateMutex.Unlock()
|
||||
channel.MarkDirty(IncludeSettings)
|
||||
}
|
||||
|
||||
func (channel *Channel) setForward(forward string) {
|
||||
@ -565,3 +825,268 @@ func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
|
||||
defer channel.stateMutex.RUnlock()
|
||||
return channel.accountToUMode[cfaccount]
|
||||
}
|
||||
|
||||
func (channel *Channel) UUID() utils.UUID {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
return channel.uuid
|
||||
}
|
||||
|
||||
func (session *Session) isSubscribedTo(key string) bool {
|
||||
session.client.stateMutex.RLock()
|
||||
defer session.client.stateMutex.RUnlock()
|
||||
|
||||
return session.metadataSubscriptions.Has(key)
|
||||
}
|
||||
|
||||
func (session *Session) SubscribeTo(keys ...string) ([]string, error) {
|
||||
maxSubs := session.client.server.Config().Metadata.MaxSubs
|
||||
|
||||
session.client.stateMutex.Lock()
|
||||
defer session.client.stateMutex.Unlock()
|
||||
|
||||
if session.metadataSubscriptions == nil {
|
||||
session.metadataSubscriptions = make(utils.HashSet[string])
|
||||
}
|
||||
|
||||
var added []string
|
||||
|
||||
for _, k := range keys {
|
||||
if !session.metadataSubscriptions.Has(k) {
|
||||
if len(session.metadataSubscriptions) > maxSubs {
|
||||
return added, errMetadataTooManySubs
|
||||
}
|
||||
added = append(added, k)
|
||||
session.metadataSubscriptions.Add(k)
|
||||
}
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
func (session *Session) UnsubscribeFrom(keys ...string) []string {
|
||||
session.client.stateMutex.Lock()
|
||||
defer session.client.stateMutex.Unlock()
|
||||
|
||||
var removed []string
|
||||
|
||||
for k := range session.metadataSubscriptions {
|
||||
if slices.Contains(keys, k) {
|
||||
removed = append(removed, k)
|
||||
session.metadataSubscriptions.Remove(k)
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
func (session *Session) MetadataSubscriptions() utils.HashSet[string] {
|
||||
session.client.stateMutex.Lock()
|
||||
defer session.client.stateMutex.Unlock()
|
||||
|
||||
return maps.Clone(session.metadataSubscriptions)
|
||||
}
|
||||
|
||||
func (channel *Channel) GetMetadata(key string) (string, bool) {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
val, ok := channel.metadata[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (channel *Channel) SetMetadata(key string, value string, limit int) (updated bool, err error) {
|
||||
defer channel.MarkDirty(IncludeAllAttrs)
|
||||
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
if channel.metadata == nil {
|
||||
channel.metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
existing, ok := channel.metadata[key]
|
||||
if !ok && len(channel.metadata) >= limit {
|
||||
return false, errLimitExceeded
|
||||
}
|
||||
updated = !ok || value != existing
|
||||
if updated {
|
||||
channel.metadata[key] = value
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (channel *Channel) ListMetadata() map[string]string {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
return maps.Clone(channel.metadata)
|
||||
}
|
||||
|
||||
func (channel *Channel) DeleteMetadata(key string) (updated bool) {
|
||||
defer channel.MarkDirty(IncludeAllAttrs)
|
||||
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
_, updated = channel.metadata[key]
|
||||
if updated {
|
||||
delete(channel.metadata, key)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func (channel *Channel) ClearMetadata() map[string]string {
|
||||
defer channel.MarkDirty(IncludeAllAttrs)
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
oldMap := channel.metadata
|
||||
channel.metadata = nil
|
||||
|
||||
return oldMap
|
||||
}
|
||||
|
||||
func (channel *Channel) CountMetadata() int {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
return len(channel.metadata)
|
||||
}
|
||||
|
||||
func (client *Client) GetMetadata(key string) (string, bool) {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
||||
val, ok := client.metadata[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) {
|
||||
var alwaysOn bool
|
||||
defer func() {
|
||||
if alwaysOn && updated {
|
||||
client.markDirty(IncludeMetadata)
|
||||
}
|
||||
}()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
alwaysOn = client.registered && client.alwaysOn
|
||||
|
||||
if client.metadata == nil {
|
||||
client.metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
existing, ok := client.metadata[key]
|
||||
if !ok && len(client.metadata) >= limit {
|
||||
return false, errLimitExceeded
|
||||
}
|
||||
updated = !ok || value != existing
|
||||
if updated {
|
||||
client.metadata[key] = value
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, limit int) (updates map[string]string) {
|
||||
var alwaysOn bool
|
||||
defer func() {
|
||||
if alwaysOn && len(updates) > 0 {
|
||||
client.markDirty(IncludeMetadata)
|
||||
}
|
||||
}()
|
||||
|
||||
updates = make(map[string]string, len(preregData))
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
alwaysOn = client.registered && client.alwaysOn
|
||||
|
||||
if client.metadata == nil {
|
||||
client.metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
for k, v := range preregData {
|
||||
// do not overwrite any existing keys
|
||||
_, ok := client.metadata[k]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
if len(client.metadata) >= limit {
|
||||
return // we know this is a new key
|
||||
}
|
||||
client.metadata[k] = v
|
||||
updates[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) ListMetadata() map[string]string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
||||
return maps.Clone(client.metadata)
|
||||
}
|
||||
|
||||
func (client *Client) DeleteMetadata(key string) (updated bool) {
|
||||
defer func() {
|
||||
if updated {
|
||||
client.markDirty(IncludeMetadata)
|
||||
}
|
||||
}()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
_, updated = client.metadata[key]
|
||||
if updated {
|
||||
delete(client.metadata, key)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func (client *Client) ClearMetadata() (oldMap map[string]string) {
|
||||
defer func() {
|
||||
if len(oldMap) > 0 {
|
||||
client.markDirty(IncludeMetadata)
|
||||
}
|
||||
}()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
oldMap = client.metadata
|
||||
client.metadata = nil
|
||||
|
||||
return oldMap
|
||||
}
|
||||
|
||||
func (client *Client) CountMetadata() int {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
||||
return len(client.metadata)
|
||||
}
|
||||
|
||||
func (client *Client) checkMetadataThrottle() (throttled bool, remainingTime time.Duration) {
|
||||
config := client.server.Config()
|
||||
if !config.Metadata.ClientThrottle.Enabled {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
// copy client.metadataThrottle locally and then back for processing
|
||||
var throttle connection_limits.GenericThrottle
|
||||
throttle.ThrottleDetails = client.metadataThrottle
|
||||
throttle.Duration = config.Metadata.ClientThrottle.Duration
|
||||
throttle.Limit = config.Metadata.ClientThrottle.MaxAttempts
|
||||
throttled, remainingTime = throttle.Touch()
|
||||
client.metadataThrottle = throttle.ThrottleDetails
|
||||
return
|
||||
}
|
||||
|
||||
1940
irc/handlers.go
1940
irc/handlers.go
File diff suppressed because it is too large
Load Diff
97
irc/help.go
97
irc/help.go
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/ergochat/ergo/irc/languages"
|
||||
)
|
||||
|
||||
// HelpEntryType represents the different sorts of help entries that can exist.
|
||||
@ -37,7 +37,7 @@ type HelpEntry struct {
|
||||
var (
|
||||
cmodeHelpText = `== Channel Modes ==
|
||||
|
||||
Oragono supports the following channel modes:
|
||||
Ergo supports the following channel modes:
|
||||
|
||||
+b | Client masks that are banned from the channel (e.g. *!*@127.0.0.1)
|
||||
+e | Client masks that are exempted from bans.
|
||||
@ -45,6 +45,8 @@ Oragono supports the following channel modes:
|
||||
+i | Invite-only mode, only invited clients can join the channel.
|
||||
+k | Key required when joining the channel.
|
||||
+l | Client join limit for the channel.
|
||||
+f | Users who are unable to join this channel (due to another mode) are forwarded
|
||||
to the provided channel instead.
|
||||
+m | Moderated mode, only privileged clients can talk on the channel.
|
||||
+n | No-outside-messages mode, only users that are on the channel can send
|
||||
| messages to it.
|
||||
@ -68,7 +70,7 @@ Oragono supports the following channel modes:
|
||||
+v (+) | Voice channel mode.`
|
||||
umodeHelpText = `== User Modes ==
|
||||
|
||||
Oragono supports the following user modes:
|
||||
Ergo supports the following user modes:
|
||||
|
||||
+a | User is marked as being away. This mode is set with the /AWAY command.
|
||||
+i | User is marked as invisible (their channels are hidden from whois replies).
|
||||
@ -81,10 +83,11 @@ Oragono supports the following user modes:
|
||||
+T | CTCP messages to the user are blocked.`
|
||||
snomaskHelpText = `== Server Notice Masks ==
|
||||
|
||||
Oragono supports the following server notice masks for operators:
|
||||
Ergo supports the following server notice masks for operators:
|
||||
|
||||
a | Local announcements.
|
||||
c | Local client connections.
|
||||
d | Local client disconnects.
|
||||
j | Local channel actions.
|
||||
k | Local kills.
|
||||
n | Local nick changes.
|
||||
@ -107,6 +110,13 @@ For instance, this would set the kill, oper, account and xline snomasks on dan:
|
||||
// Help contains the help strings distributed with the IRCd.
|
||||
var Help = map[string]HelpEntry{
|
||||
// Commands
|
||||
"accept": {
|
||||
text: `ACCEPT <target>
|
||||
|
||||
ACCEPT allows the target user to send you direct messages, overriding any
|
||||
restrictions that might otherwise prevent this. Currently, the only
|
||||
applicable restriction is the +R registered-only mode.`,
|
||||
},
|
||||
"ambiance": {
|
||||
text: `AMBIANCE <target> <text to be sent>
|
||||
|
||||
@ -129,14 +139,6 @@ longer away.`,
|
||||
|
||||
BATCH initiates an IRCv3 client-to-server batch. You should never need to
|
||||
issue this command manually.`,
|
||||
},
|
||||
"brb": {
|
||||
text: `BRB [message]
|
||||
|
||||
Disconnects you from the server, while instructing the server to keep you
|
||||
present for a short time window. During this window, you can either resume
|
||||
or reattach to your nickname. If [message] is sent, it is used as your away
|
||||
message (and as your quit message if you don't return in time).`,
|
||||
},
|
||||
"cap": {
|
||||
text: `CAP <subcommand> [:<capabilities>]
|
||||
@ -149,8 +151,8 @@ http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
|
||||
text: `CHATHISTORY [params]
|
||||
|
||||
CHATHISTORY is a history replay command associated with the IRCv3
|
||||
specification draft/chathistory. See this document:
|
||||
https://github.com/ircv3/ircv3-specifications/pull/393`,
|
||||
chathistory extension. See this document:
|
||||
https://ircv3.net/specs/extensions/chathistory`,
|
||||
},
|
||||
"debug": {
|
||||
oper: true,
|
||||
@ -236,11 +238,10 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
||||
"history": {
|
||||
text: `HISTORY <target> [limit]
|
||||
|
||||
Replay message history. <target> can be a channel name, "me" to replay direct
|
||||
message history, or a nickname to replay another client's direct message
|
||||
history (they must be logged into the same account as you). [limit] can be
|
||||
either an integer (the maximum number of messages to replay), or a time
|
||||
duration like 10m or 1h (the time window within which to replay messages).`,
|
||||
Replay message history. <target> can be a channel name or a nickname you have
|
||||
direct message history with. [limit] can be either an integer (the maximum
|
||||
number of messages to replay), or a time duration like 10m or 1h (the time
|
||||
window within which to replay messages).`,
|
||||
},
|
||||
"info": {
|
||||
text: `INFO
|
||||
@ -257,6 +258,11 @@ appropriate channel privs.`,
|
||||
text: `ISON <nickname>{ <nickname>}
|
||||
|
||||
Returns whether the given nicks exist on the network.`,
|
||||
},
|
||||
"isupport": {
|
||||
text: `ISUPPORT
|
||||
|
||||
Returns RPL_ISUPPORT lines describing the server's capabilities.`,
|
||||
},
|
||||
"join": {
|
||||
text: `JOIN <channel>{,<channel>} [<key>{,<key>}]
|
||||
@ -325,6 +331,19 @@ channels). <elistcond>s modify how the channels are selected.`,
|
||||
Shows statistics about the size of the network. If <mask> is given, only
|
||||
returns stats for servers matching the given mask. If <server> is given, the
|
||||
command is processed by that server.`,
|
||||
},
|
||||
"markread": {
|
||||
text: `MARKREAD <target> [timestamp]
|
||||
|
||||
MARKREAD updates an IRCv3 read message marker. It is not intended for use by
|
||||
end users. For more details, see the latest draft of the read-marker
|
||||
specification.`,
|
||||
},
|
||||
"metadata": {
|
||||
text: `METADATA <target> <subcommand> [<everything else>...]
|
||||
|
||||
Retrieve and meddle with metadata for the given target.
|
||||
Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`,
|
||||
},
|
||||
"mode": {
|
||||
text: `MODE <target> [<modestring> [<mode arguments>...]]
|
||||
@ -404,6 +423,13 @@ Leaves the given channels and shows people the given reason.`,
|
||||
|
||||
When the server requires a connection password to join, used to send us the
|
||||
password.`,
|
||||
},
|
||||
"persistence": {
|
||||
text: `PERSISTENCE [params]
|
||||
|
||||
PERSISTENCE is a command associated with an IRC protocol extension for
|
||||
persistent connections. End users should probably use /NS GET ALWAYS-ON
|
||||
and /NS SET ALWAYS-ON instead.`,
|
||||
},
|
||||
"ping": {
|
||||
text: `PING <args>...
|
||||
@ -419,6 +445,12 @@ Replies to a PING. Used to check link connectivity.`,
|
||||
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
||||
|
||||
Sends the text to the given targets as a PRIVMSG.`,
|
||||
},
|
||||
"redact": {
|
||||
text: `REDACT <target> <targetmsgid> [<reason>]
|
||||
|
||||
Removes the message of the target msgid from the chat history of a channel
|
||||
or target user.`,
|
||||
},
|
||||
"relaymsg": {
|
||||
text: `RELAYMSG <channel> <spoofed nick> :<message>
|
||||
@ -486,21 +518,15 @@ specs for more info: http://ircv3.net/specs/core/message-tags-3.3.html`,
|
||||
Indicates that you're leaving the server, and shows everyone the given reason.`,
|
||||
},
|
||||
"register": {
|
||||
text: `REGISTER <email | *> <password>
|
||||
text: `REGISTER <account> <email | *> <password>
|
||||
|
||||
Registers an account in accordance with the draft/register capability.`,
|
||||
Registers an account in accordance with the draft/account-registration capability.`,
|
||||
},
|
||||
"rehash": {
|
||||
oper: true,
|
||||
text: `REHASH
|
||||
|
||||
Reloads the config file and updates TLS certificates on listeners`,
|
||||
},
|
||||
"resume": {
|
||||
text: `RESUME <oldnick> [timestamp]
|
||||
|
||||
Sent before registration has completed, this indicates that the client wants to
|
||||
resume their old connection <oldnick>.`,
|
||||
},
|
||||
"time": {
|
||||
text: `TIME [server]
|
||||
@ -516,7 +542,7 @@ given, views the current topic on the channel.`,
|
||||
"uban": {
|
||||
text: `UBAN <subcommand> [arguments]
|
||||
|
||||
Oragono's "unified ban" system. Accepts the following subcommands:
|
||||
Ergo's "unified ban" system. Accepts the following subcommands:
|
||||
|
||||
1. UBAN ADD <target> [REQUIRE-SASL] [DURATION <duration>] [REASON...]
|
||||
2. UBAN DEL <target>
|
||||
@ -568,9 +594,9 @@ The USERS command is not implemented.`,
|
||||
Shows information about the given users. Takes up to 10 nicknames.`,
|
||||
},
|
||||
"verify": {
|
||||
text: `VERIFY <account> <password>
|
||||
text: `VERIFY <account> <code>
|
||||
|
||||
Verifies an account in accordance with the draft/register capability.`,
|
||||
Verifies an account in accordance with the draft/account-registration capability.`,
|
||||
},
|
||||
"version": {
|
||||
text: `VERSION [server]
|
||||
@ -589,6 +615,11 @@ ircv3.net/specs/extensions/webirc.html
|
||||
the connection from the client to the gateway, such as:
|
||||
|
||||
- tls: this flag indicates that the client->gateway connection is secure`,
|
||||
},
|
||||
"webpush": {
|
||||
text: `WEBPUSH <subcommand> [arguments]
|
||||
|
||||
Configures web push settings. Not for direct use by end users.`,
|
||||
},
|
||||
"who": {
|
||||
text: `WHO <name> [o]
|
||||
@ -652,15 +683,15 @@ for direct use by end users.`,
|
||||
"casemapping": {
|
||||
text: `RPL_ISUPPORT CASEMAPPING
|
||||
|
||||
Oragono supports an experimental unicode casemapping designed for extended
|
||||
Ergo supports an experimental unicode casemapping designed for extended
|
||||
Unicode support. This casemapping is based off RFC 7613 and the draft rfc7613
|
||||
casemapping spec here: https://oragono.io/specs.html`,
|
||||
casemapping spec here: https://ergo.chat/specs.html`,
|
||||
helpType: ISupportHelpEntry,
|
||||
},
|
||||
"prefix": {
|
||||
text: `RPL_ISUPPORT PREFIX
|
||||
|
||||
Oragono supports the following channel membership prefixes:
|
||||
Ergo supports the following channel membership prefixes:
|
||||
|
||||
+q (~) | Founder channel mode.
|
||||
+a (&) | Admin channel mode.
|
||||
|
||||
@ -4,9 +4,11 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type ItemType uint
|
||||
@ -44,7 +46,8 @@ type Item struct {
|
||||
// for a DM, this is the casefolded nickname of the other party (whether this is
|
||||
// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
|
||||
// required by CHATHISTORY:
|
||||
CfCorrespondent string
|
||||
CfCorrespondent string `json:"CfCorrespondent,omitempty"`
|
||||
IsBot bool `json:"IsBot,omitempty"`
|
||||
}
|
||||
|
||||
// HasMsgid tests whether a message has the message id `msgid`.
|
||||
@ -54,12 +57,6 @@ func (item *Item) HasMsgid(msgid string) bool {
|
||||
|
||||
type Predicate func(item *Item) (matches bool)
|
||||
|
||||
func Reverse(results []Item) {
|
||||
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
||||
results[i], results[j] = results[j], results[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer is a ring buffer holding message/event history for a channel or user
|
||||
type Buffer struct {
|
||||
sync.RWMutex
|
||||
@ -159,7 +156,7 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
|
||||
|
||||
defer func() {
|
||||
if !ascending {
|
||||
Reverse(results)
|
||||
slices.Reverse(results)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -200,6 +197,78 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
|
||||
return list.matchInternal(satisfies, ascending, limit), complete, nil
|
||||
}
|
||||
|
||||
// returns all correspondents, in reverse time order
|
||||
func (list *Buffer) allCorrespondents() (results []TargetListing) {
|
||||
seen := make(utils.HashSet[string])
|
||||
|
||||
list.RLock()
|
||||
defer list.RUnlock()
|
||||
if list.start == -1 || len(list.buffer) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// XXX traverse in reverse order, so we get the latest timestamp
|
||||
// of any message sent to/from the correspondent
|
||||
pos := list.prev(list.end)
|
||||
stop := list.start
|
||||
|
||||
for {
|
||||
if !seen.Has(list.buffer[pos].CfCorrespondent) {
|
||||
seen.Add(list.buffer[pos].CfCorrespondent)
|
||||
results = append(results, TargetListing{
|
||||
CfName: list.buffer[pos].CfCorrespondent,
|
||||
Time: list.buffer[pos].Message.Time,
|
||||
})
|
||||
}
|
||||
|
||||
if pos == stop {
|
||||
break
|
||||
}
|
||||
pos = list.prev(pos)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// list DM correspondents, as one input to CHATHISTORY TARGETS
|
||||
func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []TargetListing, err error) {
|
||||
after := start.Time
|
||||
before := end.Time
|
||||
after, before, ascending := MinMaxAsc(after, before, cutoff)
|
||||
|
||||
correspondents := list.allCorrespondents()
|
||||
if len(correspondents) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// XXX allCorrespondents returns results in reverse order,
|
||||
// so if we're ascending, we actually go backwards
|
||||
var i int
|
||||
if ascending {
|
||||
i = len(correspondents) - 1
|
||||
} else {
|
||||
i = 0
|
||||
}
|
||||
|
||||
for 0 <= i && i < len(correspondents) && (limit == 0 || len(results) < limit) {
|
||||
if (after.IsZero() || correspondents[i].Time.After(after)) &&
|
||||
(before.IsZero() || correspondents[i].Time.Before(before)) {
|
||||
results = append(results, correspondents[i])
|
||||
}
|
||||
|
||||
if ascending {
|
||||
i--
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if !ascending {
|
||||
slices.Reverse(results)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// implements history.Sequence, emulating a single history buffer (for a channel,
|
||||
// a single user's DMs, or a DM conversation)
|
||||
type bufferSequence struct {
|
||||
@ -222,14 +291,27 @@ func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequenc
|
||||
}
|
||||
}
|
||||
|
||||
func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
|
||||
return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
|
||||
func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, err error) {
|
||||
results, _, err = seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
|
||||
return
|
||||
}
|
||||
|
||||
func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
|
||||
return GenericAround(seq, start, limit)
|
||||
}
|
||||
|
||||
func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
|
||||
return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
|
||||
}
|
||||
|
||||
func (seq *bufferSequence) Cutoff() time.Time {
|
||||
return seq.cutoff
|
||||
}
|
||||
|
||||
func (seq *bufferSequence) Ephemeral() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// you must be holding the read lock to call this
|
||||
func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
|
||||
if list.start == -1 || len(list.buffer) == 0 {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -17,15 +18,24 @@ type Selector struct {
|
||||
// it encapsulates restrictions such as registration time cutoffs, or
|
||||
// only looking at a single "query buffer" (DMs with a particular correspondent)
|
||||
type Sequence interface {
|
||||
Between(start, end Selector, limit int) (results []Item, complete bool, err error)
|
||||
Between(start, end Selector, limit int) (results []Item, err error)
|
||||
Around(start Selector, limit int) (results []Item, err error)
|
||||
|
||||
ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error)
|
||||
|
||||
// this are weird hacks that violate the encapsulation of Sequence to some extent;
|
||||
// Cutoff() returns the cutoff time for other code to use (it returns the zero time
|
||||
// if none is set), and Ephemeral() returns whether the backing store is in-memory
|
||||
// or a persistent database.
|
||||
Cutoff() time.Time
|
||||
Ephemeral() bool
|
||||
}
|
||||
|
||||
// This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
|
||||
func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
|
||||
var halfLimit int
|
||||
halfLimit = (limit + 1) / 2
|
||||
initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
|
||||
initialResults, err := seq.Between(Selector{}, start, halfLimit)
|
||||
if err != nil {
|
||||
return
|
||||
} else if len(initialResults) == 0 {
|
||||
@ -34,7 +44,7 @@ func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err
|
||||
return
|
||||
}
|
||||
newStart := Selector{Time: initialResults[0].Message.Time}
|
||||
results, _, err = seq.Between(newStart, Selector{}, limit)
|
||||
results, err = seq.Between(newStart, Selector{}, limit)
|
||||
return
|
||||
}
|
||||
|
||||
@ -68,3 +78,16 @@ func MinMaxAsc(after, before, cutoff time.Time) (min, max time.Time, ascending b
|
||||
}
|
||||
return after, before, ascending
|
||||
}
|
||||
|
||||
// maps regular msgids from JOIN, etc. to a msgid suitable for attaching
|
||||
// to a HistServ message describing the JOIN. See #491 for some history.
|
||||
func HistservMungeMsgid(msgid string) string {
|
||||
return "_" + msgid
|
||||
}
|
||||
|
||||
// strips munging from a msgid. future schemes may not support a well-defined
|
||||
// mapping of munged msgids to true msgids, but munged msgids should always contain
|
||||
// a _, with metadata in front and data (possibly the true msgid) after.
|
||||
func NormalizeMsgid(msgid string) string {
|
||||
return strings.TrimPrefix(msgid, "_")
|
||||
}
|
||||
|
||||
77
irc/history/targets.go
Normal file
77
irc/history/targets.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2021 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package history
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TargetListing struct {
|
||||
CfName string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// Merge `base`, a paging window of targets, with `extras` (the target entries
|
||||
// for all joined channels).
|
||||
func MergeTargets(base []TargetListing, extra []TargetListing, start, end time.Time, limit int) (results []TargetListing) {
|
||||
if len(extra) == 0 {
|
||||
return base
|
||||
}
|
||||
SortCorrespondents(extra)
|
||||
|
||||
start, end, ascending := MinMaxAsc(start, end, time.Time{})
|
||||
predicate := func(t time.Time) bool {
|
||||
return (start.IsZero() || start.Before(t)) && (end.IsZero() || end.After(t))
|
||||
}
|
||||
|
||||
prealloc := len(base) + len(extra)
|
||||
if limit < prealloc {
|
||||
prealloc = limit
|
||||
}
|
||||
results = make([]TargetListing, 0, prealloc)
|
||||
|
||||
if !ascending {
|
||||
slices.Reverse(base)
|
||||
slices.Reverse(extra)
|
||||
}
|
||||
|
||||
for len(results) < limit {
|
||||
if len(extra) != 0 {
|
||||
if !predicate(extra[0].Time) {
|
||||
extra = extra[1:]
|
||||
continue
|
||||
}
|
||||
if len(base) != 0 {
|
||||
if base[0].Time.Before(extra[0].Time) == ascending {
|
||||
results = append(results, base[0])
|
||||
base = base[1:]
|
||||
} else {
|
||||
results = append(results, extra[0])
|
||||
extra = extra[1:]
|
||||
}
|
||||
} else {
|
||||
results = append(results, extra[0])
|
||||
extra = extra[1:]
|
||||
}
|
||||
} else if len(base) != 0 {
|
||||
results = append(results, base[0])
|
||||
base = base[1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ascending {
|
||||
slices.Reverse(results)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SortCorrespondents(list []TargetListing) {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].Time.Before(list[j].Time)
|
||||
})
|
||||
}
|
||||
@ -7,13 +7,20 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/history"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/history"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type CanDelete uint
|
||||
|
||||
const (
|
||||
canDeleteAny CanDelete = iota // User is allowed to delete any message (for a given channel/PM)
|
||||
canDeleteSelf // User is allowed to delete their own messages (ditto)
|
||||
canDeleteNone // User is not allowed to delete any message (ditto)
|
||||
)
|
||||
|
||||
const (
|
||||
@ -43,14 +50,13 @@ FORGET deletes all history messages sent by an account.`,
|
||||
},
|
||||
"delete": {
|
||||
handler: histservDeleteHandler,
|
||||
help: `Syntax: $bDELETE [target] <msgid>$b
|
||||
help: `Syntax: $bDELETE <target> <msgid>$b
|
||||
|
||||
DELETE deletes an individual message by its msgid. The target is a channel
|
||||
name or nickname; depending on the history implementation, this may or may not
|
||||
be necessary to locate the message.`,
|
||||
helpShort: `$bDELETE$b deletes an individual message by its msgid.`,
|
||||
DELETE deletes an individual message by its msgid. The target is the channel
|
||||
name. The msgid is the ID as can be found in the tags of that message.`,
|
||||
helpShort: `$bDELETE$b deletes an individual message by its target and msgid.`,
|
||||
enabled: histservEnabled,
|
||||
minParams: 1,
|
||||
minParams: 2,
|
||||
maxParams: 2,
|
||||
},
|
||||
"export": {
|
||||
@ -70,7 +76,7 @@ the request of the account holder.`,
|
||||
help: `Syntax: $bPLAY <target> [limit]$b
|
||||
|
||||
PLAY plays back history messages, rendering them into direct messages from
|
||||
HistServ. 'target' is a channel name (or 'me' for direct messages), and 'limit'
|
||||
HistServ. 'target' is a channel name or nickname to query, and 'limit'
|
||||
is a message count or a time duration. Note that message playback may be
|
||||
incomplete or degraded, relative to direct playback from /HISTORY or
|
||||
CHATHISTORY.`,
|
||||
@ -94,19 +100,43 @@ func histservForgetHandler(service *ircService, server *Server, client *Client,
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName))
|
||||
}
|
||||
|
||||
func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
var target, msgid string
|
||||
if len(params) == 1 {
|
||||
msgid = params[0]
|
||||
// Returns:
|
||||
//
|
||||
// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.:
|
||||
// - the client is a channel operator, or
|
||||
// - the client is an operator with "history" capability
|
||||
//
|
||||
// 2. `canDeleteSelf` if the client is allowed to delete their own messages from the target
|
||||
// 3. `canDeleteNone` otherwise
|
||||
func deletionPolicy(server *Server, client *Client, target string) CanDelete {
|
||||
isOper := client.HasRoleCapabs("history")
|
||||
if isOper {
|
||||
return canDeleteAny
|
||||
} else {
|
||||
target, msgid = params[0], params[1]
|
||||
if server.Config().History.Retention.AllowIndividualDelete {
|
||||
channel := server.channels.Get(target)
|
||||
if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) {
|
||||
return canDeleteAny
|
||||
} else {
|
||||
return canDeleteSelf
|
||||
}
|
||||
} else {
|
||||
return canDeleteNone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
target, msgid := params[0], params[1] // Fix #1881 2 params are required
|
||||
|
||||
canDelete := deletionPolicy(server, client, target)
|
||||
accountName := "*"
|
||||
hasPrivs := client.HasRoleCapabs("history")
|
||||
if !hasPrivs {
|
||||
if canDelete == canDeleteNone {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
} else if canDelete == canDeleteSelf {
|
||||
accountName = client.AccountName()
|
||||
if !(server.Config().History.Retention.AllowIndividualDelete && accountName != "*") {
|
||||
if accountName == "*" {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
@ -116,7 +146,8 @@ func histservDeleteHandler(service *ircService, server *Server, client *Client,
|
||||
if err == nil {
|
||||
service.Notice(rb, client.t("Successfully deleted message"))
|
||||
} else {
|
||||
if hasPrivs {
|
||||
isOper := client.HasRoleCapabs("history")
|
||||
if isOper {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
||||
} else {
|
||||
service.Notice(rb, client.t("Could not delete message"))
|
||||
@ -133,7 +164,7 @@ func histservExportHandler(service *ircService, server *Server, client *Client,
|
||||
|
||||
config := server.Config()
|
||||
// don't include the account name in the filename because of escaping concerns
|
||||
filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(IRCv3TimestampFormat))
|
||||
filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(utils.IRCv3TimestampFormat))
|
||||
pathname := config.getOutputPath(filename)
|
||||
outfile, err := os.Create(pathname)
|
||||
if err != nil {
|
||||
@ -146,12 +177,7 @@ func histservExportHandler(service *ircService, server *Server, client *Client,
|
||||
}
|
||||
|
||||
func histservExportAndNotify(service *ircService, server *Server, cfAccount string, outfile *os.File, filename, alertNick string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
server.logger.Error("history",
|
||||
fmt.Sprintf("Panic in history export routine: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
}()
|
||||
defer server.HandlePanic(nil)
|
||||
|
||||
defer outfile.Close()
|
||||
writer := bufio.NewWriter(outfile)
|
||||
@ -195,11 +221,7 @@ func histservPlayHandler(service *ircService, server *Server, client *Client, co
|
||||
|
||||
// handles parameter parsing and history queries for /HISTORY and /HISTSERV PLAY
|
||||
func easySelectHistory(server *Server, client *Client, params []string) (items []history.Item, channel *Channel, err error) {
|
||||
target := params[0]
|
||||
if strings.ToLower(target) == "me" {
|
||||
target = "*"
|
||||
}
|
||||
channel, sequence, err := server.GetHistorySequence(nil, client, target)
|
||||
channel, sequence, err := server.GetHistorySequence(nil, client, params[0])
|
||||
|
||||
if sequence == nil || err != nil {
|
||||
return nil, nil, errNoSuchChannel
|
||||
@ -227,12 +249,12 @@ func easySelectHistory(server *Server, client *Client, params []string) (items [
|
||||
}
|
||||
|
||||
if duration == 0 {
|
||||
items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
|
||||
items, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
|
||||
} else {
|
||||
now := time.Now().UTC()
|
||||
start := history.Selector{Time: now}
|
||||
end := history.Selector{Time: now.Add(-duration)}
|
||||
items, _, err = sequence.Between(start, end, limit)
|
||||
items, err = sequence.Between(start, end, limit)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -8,9 +8,10 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/ergochat/irc-go/ircfmt"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/sno"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -53,16 +54,16 @@ OFF disables your vhost, if you have one approved.`,
|
||||
handler: hsStatusHandler,
|
||||
help: `Syntax: $bSTATUS [user]$b
|
||||
|
||||
STATUS displays your current vhost, if any, and the status of your most recent
|
||||
request for a new one. A server operator can view someone else's status.`,
|
||||
helpShort: `$bSTATUS$b shows your vhost and request status.`,
|
||||
STATUS displays your current vhost, if any, and whether it is enabled or
|
||||
disabled. A server operator can view someone else's status.`,
|
||||
helpShort: `$bSTATUS$b shows your vhost status.`,
|
||||
enabled: hostservEnabled,
|
||||
},
|
||||
"set": {
|
||||
handler: hsSetHandler,
|
||||
help: `Syntax: $bSET <user> <vhost>$b
|
||||
|
||||
SET sets a user's vhost, bypassing the request system.`,
|
||||
SET sets a user's vhost.`,
|
||||
helpShort: `$bSET$b sets a user's vhost.`,
|
||||
capabs: []string{"vhosts"},
|
||||
enabled: hostservEnabled,
|
||||
@ -159,6 +160,7 @@ func validateVhost(server *Server, vhost string, oper bool) error {
|
||||
}
|
||||
|
||||
func hsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
oper := client.Oper()
|
||||
user := params[0]
|
||||
var vhost string
|
||||
|
||||
@ -176,8 +178,10 @@ func hsSetHandler(service *ircService, server *Server, client *Client, command s
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
} else if vhost != "" {
|
||||
service.Notice(rb, client.t("Successfully set vhost"))
|
||||
server.snomasks.Send(sno.LocalVhosts, fmt.Sprintf("Operator %[1]s set vhost %[2]s on account %[3]s", oper.Name, vhost, user))
|
||||
} else {
|
||||
service.Notice(rb, client.t("Successfully cleared vhost"))
|
||||
server.snomasks.Send(sno.LocalVhosts, fmt.Sprintf("Operator %[1]s cleared vhost on account %[2]s", oper.Name, user))
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,6 +193,6 @@ func hsSetCloakSecretHandler(service *ircService, server *Server, client *Client
|
||||
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
|
||||
return
|
||||
}
|
||||
StoreCloakSecret(server.store, secret)
|
||||
StoreCloakSecret(server.dstore, secret)
|
||||
service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
|
||||
}
|
||||
|
||||
133
irc/idletimer.go
133
irc/idletimer.go
@ -1,133 +0,0 @@
|
||||
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BrbTimer is a timer on the client as a whole (not an individual session) for implementing
|
||||
// the BRB command and related functionality (where a client can remain online without
|
||||
// having any connected sessions).
|
||||
|
||||
type BrbState uint
|
||||
|
||||
const (
|
||||
// BrbDisabled is the default state; the client will be disconnected if it has no sessions
|
||||
BrbDisabled BrbState = iota
|
||||
// BrbEnabled allows the client to remain online without sessions; if a timeout is
|
||||
// reached, it will be removed
|
||||
BrbEnabled
|
||||
// BrbDead is the state of a client after its timeout has expired; it will be removed
|
||||
// and therefore new sessions cannot be attached to it
|
||||
BrbDead
|
||||
)
|
||||
|
||||
type BrbTimer struct {
|
||||
// XXX we use client.stateMutex for synchronization, so we can atomically test
|
||||
// conditions that use both brbTimer.state and client.sessions. This code
|
||||
// is tightly coupled with the rest of Client.
|
||||
client *Client
|
||||
|
||||
state BrbState
|
||||
brbAt time.Time
|
||||
duration time.Duration
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
func (bt *BrbTimer) Initialize(client *Client) {
|
||||
bt.client = client
|
||||
}
|
||||
|
||||
// attempts to enable BRB for a client, returns whether it succeeded
|
||||
func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
|
||||
// TODO make this configurable
|
||||
duration = ResumeableTotalTimeout
|
||||
|
||||
bt.client.stateMutex.Lock()
|
||||
defer bt.client.stateMutex.Unlock()
|
||||
|
||||
if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
switch bt.state {
|
||||
case BrbDisabled, BrbEnabled:
|
||||
bt.state = BrbEnabled
|
||||
bt.duration = duration
|
||||
bt.resetTimeout()
|
||||
// only track the earliest BRB, if multiple sessions are BRB'ing at once
|
||||
// TODO(#524) this is inaccurate in case of an auto-BRB
|
||||
if bt.brbAt.IsZero() {
|
||||
bt.brbAt = time.Now().UTC()
|
||||
}
|
||||
success = true
|
||||
default:
|
||||
// BrbDead
|
||||
success = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// turns off BRB for a client and stops the timer; used on resume and during
|
||||
// client teardown
|
||||
func (bt *BrbTimer) Disable() (brbAt time.Time) {
|
||||
bt.client.stateMutex.Lock()
|
||||
defer bt.client.stateMutex.Unlock()
|
||||
|
||||
if bt.state == BrbEnabled {
|
||||
bt.state = BrbDisabled
|
||||
brbAt = bt.brbAt
|
||||
bt.brbAt = time.Time{}
|
||||
}
|
||||
bt.resetTimeout()
|
||||
return
|
||||
}
|
||||
|
||||
func (bt *BrbTimer) resetTimeout() {
|
||||
if bt.timer != nil {
|
||||
bt.timer.Stop()
|
||||
}
|
||||
if bt.state != BrbEnabled {
|
||||
return
|
||||
}
|
||||
if bt.timer == nil {
|
||||
bt.timer = time.AfterFunc(bt.duration, bt.processTimeout)
|
||||
} else {
|
||||
bt.timer.Reset(bt.duration)
|
||||
}
|
||||
}
|
||||
|
||||
func (bt *BrbTimer) processTimeout() {
|
||||
dead := false
|
||||
defer func() {
|
||||
if dead {
|
||||
bt.client.Quit(bt.client.AwayMessage(), nil)
|
||||
bt.client.destroy(nil)
|
||||
}
|
||||
}()
|
||||
|
||||
bt.client.stateMutex.Lock()
|
||||
defer bt.client.stateMutex.Unlock()
|
||||
|
||||
if bt.client.alwaysOn {
|
||||
return
|
||||
}
|
||||
|
||||
switch bt.state {
|
||||
case BrbDisabled, BrbEnabled:
|
||||
if len(bt.client.sessions) == 0 {
|
||||
// client never returned, quit them
|
||||
bt.state = BrbDead
|
||||
dead = true
|
||||
} else {
|
||||
// client resumed, reattached, or has another active session
|
||||
bt.state = BrbDisabled
|
||||
bt.brbAt = time.Time{}
|
||||
}
|
||||
case BrbDead:
|
||||
dead = true // shouldn't be possible but whatever
|
||||
}
|
||||
bt.resetTimeout()
|
||||
}
|
||||
@ -6,13 +6,18 @@ package irc
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/bunt"
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/webpush"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -20,7 +25,7 @@ const (
|
||||
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
||||
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
||||
// db of the hardcoded version)
|
||||
importDBSchemaVersion = 19
|
||||
importDBSchemaVersion = 24
|
||||
)
|
||||
|
||||
type userImport struct {
|
||||
@ -54,8 +59,8 @@ type databaseImport struct {
|
||||
Channels map[string]channelImport
|
||||
}
|
||||
|
||||
func serializeAmodes(raw map[string]string, validCfUsernames utils.StringSet) (result []byte, err error) {
|
||||
processed := make(map[string]int, len(raw))
|
||||
func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
|
||||
result = make(map[string]modes.Mode)
|
||||
for accountName, mode := range raw {
|
||||
if len(mode) != 1 {
|
||||
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
|
||||
@ -64,10 +69,9 @@ func serializeAmodes(raw map[string]string, validCfUsernames utils.StringSet) (r
|
||||
if err != nil || !validCfUsernames.Has(cfname) {
|
||||
log.Printf("skipping invalid amode recipient %s\n", accountName)
|
||||
} else {
|
||||
processed[cfname] = int(mode[0])
|
||||
result[cfname] = modes.Mode(mode[0])
|
||||
}
|
||||
}
|
||||
result, err = json.Marshal(processed)
|
||||
return
|
||||
}
|
||||
|
||||
@ -79,8 +83,17 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||
|
||||
tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil)
|
||||
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
|
||||
vapidKeys, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vapidKeysJSON, err := json.Marshal(vapidKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Set(keyVAPIDKeys, string(vapidKeysJSON), nil)
|
||||
|
||||
cfUsernames := make(utils.StringSet)
|
||||
cfUsernames := make(utils.HashSet[string])
|
||||
skeletonToUsername := make(map[string]string)
|
||||
warnSkeletons := false
|
||||
|
||||
@ -121,7 +134,9 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||
tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountCallback, cfUsername), "mailto:"+userInfo.Email, nil)
|
||||
settings := AccountSettings{Email: userInfo.Email}
|
||||
settingsBytes, _ := json.Marshal(settings)
|
||||
tx.Set(fmt.Sprintf(keyAccountSettings, cfUsername), string(settingsBytes), nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil)
|
||||
if userInfo.Vhost != "" {
|
||||
@ -145,8 +160,9 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||
cfUsernames.Add(cfUsername)
|
||||
}
|
||||
|
||||
// TODO fix this:
|
||||
for chname, chInfo := range dbImport.Channels {
|
||||
cfchname, err := CasefoldChannel(chname)
|
||||
_, err := CasefoldChannel(chname)
|
||||
if err != nil {
|
||||
log.Printf("invalid channel name %s: %v", chname, err)
|
||||
continue
|
||||
@ -156,43 +172,42 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
|
||||
continue
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil)
|
||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder)
|
||||
founderChannels, fcErr := tx.Get(accountChannelsKey)
|
||||
if fcErr != nil || founderChannels == "" {
|
||||
founderChannels = cfchname
|
||||
} else {
|
||||
founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname)
|
||||
}
|
||||
tx.Set(accountChannelsKey, founderChannels, nil)
|
||||
var regInfo RegisteredChannel
|
||||
regInfo.Name = chname
|
||||
regInfo.UUID = utils.GenerateUUIDv4()
|
||||
regInfo.Founder = cffounder
|
||||
regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
|
||||
if chInfo.Topic != "" {
|
||||
tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil)
|
||||
regInfo.Topic = chInfo.Topic
|
||||
regInfo.TopicSetBy = chInfo.TopicSetBy
|
||||
regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
|
||||
}
|
||||
|
||||
if len(chInfo.Amode) != 0 {
|
||||
m, err := serializeAmodes(chInfo.Amode, cfUsernames)
|
||||
m, err := convertAmodes(chInfo.Amode, cfUsernames)
|
||||
if err == nil {
|
||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
|
||||
regInfo.AccountToUMode = m
|
||||
} else {
|
||||
log.Printf("couldn't serialize amodes for %s: %v", chname, err)
|
||||
log.Printf("couldn't process amodes for %s: %v", chname, err)
|
||||
}
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyChannelModes, cfchname), chInfo.Modes, nil)
|
||||
if chInfo.Key != "" {
|
||||
tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil)
|
||||
for _, mode := range chInfo.Modes {
|
||||
regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
|
||||
}
|
||||
regInfo.Key = chInfo.Key
|
||||
if chInfo.Limit > 0 {
|
||||
tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil)
|
||||
regInfo.UserLimit = chInfo.Limit
|
||||
}
|
||||
if chInfo.Forward != "" {
|
||||
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
|
||||
tx.Set(fmt.Sprintf(keyChannelForward, cfchname), chInfo.Forward, nil)
|
||||
regInfo.Forward = chInfo.Forward
|
||||
}
|
||||
}
|
||||
if j, err := json.Marshal(regInfo); err == nil {
|
||||
tx.Set(bunt.BuntKey(datastore.TableChannels, regInfo.UUID), string(j), nil)
|
||||
} else {
|
||||
log.Printf("couldn't serialize channel %s: %v", chname, err)
|
||||
}
|
||||
}
|
||||
|
||||
if warnSkeletons {
|
||||
@ -215,7 +230,7 @@ func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err err
|
||||
}
|
||||
|
||||
func ImportDB(config *Config, infile string) (err error) {
|
||||
data, err := ioutil.ReadFile(infile)
|
||||
data, err := os.ReadFile(infile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -240,5 +255,7 @@ func ImportDB(config *Config, infile string) (err error) {
|
||||
return doImportDB(config, dbImport, tx)
|
||||
}
|
||||
|
||||
return db.Update(performImport)
|
||||
err = db.Update(performImport)
|
||||
db.Close()
|
||||
return
|
||||
}
|
||||
|
||||
158
irc/ircconn.go
158
irc/ircconn.go
@ -5,28 +5,30 @@ package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircreader"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
maxReadQBytes = ircmsg.MaxlenTagsFromClient + MaxLineLen + 1024
|
||||
initialBufferSize = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
crlf = []byte{'\r', '\n'}
|
||||
errReadQ = errors.New("ReadQ Exceeded")
|
||||
errWSBinaryMessage = errors.New("WebSocket binary messages are unsupported")
|
||||
crlf = []byte{'\r', '\n'}
|
||||
)
|
||||
|
||||
// maximum total length, in bytes, of a single IRC message:
|
||||
func maxReadQBytes() int {
|
||||
return ircmsg.MaxlenTagsFromClient + MaxLineLen + 1024
|
||||
}
|
||||
|
||||
// IRCConn abstracts away the distinction between a regular
|
||||
// net.Conn (which includes both raw TCP and TLS) and a websocket.
|
||||
// it doesn't expose the net.Conn, io.Reader, or io.Writer interfaces
|
||||
@ -48,17 +50,14 @@ type IRCConn interface {
|
||||
type IRCStreamConn struct {
|
||||
conn *utils.WrappedConn
|
||||
|
||||
buf []byte
|
||||
start int // start of valid (i.e., read but not yet consumed) data in the buffer
|
||||
end int // end of valid data in the buffer
|
||||
searchFrom int // start of valid data in the buffer not yet searched for \n
|
||||
eof bool
|
||||
reader ircreader.Reader
|
||||
}
|
||||
|
||||
func NewIRCStreamConn(conn *utils.WrappedConn) *IRCStreamConn {
|
||||
return &IRCStreamConn{
|
||||
conn: conn,
|
||||
}
|
||||
var c IRCStreamConn
|
||||
c.conn = conn
|
||||
c.reader.Initialize(conn.Conn, initialBufferSize, maxReadQBytes())
|
||||
return &c
|
||||
}
|
||||
|
||||
func (cc *IRCStreamConn) UnderlyingConn() *utils.WrappedConn {
|
||||
@ -78,56 +77,13 @@ func (cc *IRCStreamConn) WriteLines(buffers [][]byte) (err error) {
|
||||
}
|
||||
|
||||
func (cc *IRCStreamConn) ReadLine() ([]byte, error) {
|
||||
for {
|
||||
// try to find a terminated line in the buffered data already read
|
||||
nlidx := bytes.IndexByte(cc.buf[cc.searchFrom:cc.end], '\n')
|
||||
if nlidx != -1 {
|
||||
// got a complete line
|
||||
line := cc.buf[cc.start : cc.searchFrom+nlidx]
|
||||
cc.start = cc.searchFrom + nlidx + 1
|
||||
cc.searchFrom = cc.start
|
||||
if globalUtf8EnforcementSetting && !utf8.Valid(line) {
|
||||
return line, errInvalidUtf8
|
||||
} else {
|
||||
return line, nil
|
||||
}
|
||||
}
|
||||
|
||||
if cc.start == 0 && len(cc.buf) == maxReadQBytes {
|
||||
return nil, errReadQ // out of space, can't expand or slide
|
||||
}
|
||||
|
||||
if cc.eof {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if len(cc.buf) < maxReadQBytes && (len(cc.buf)-(cc.end-cc.start) < initialBufferSize/2) {
|
||||
// allocate a new buffer, copy any remaining data
|
||||
newLen := utils.RoundUpToPowerOfTwo(len(cc.buf) + 1)
|
||||
if newLen > maxReadQBytes {
|
||||
newLen = maxReadQBytes
|
||||
} else if newLen < initialBufferSize {
|
||||
newLen = initialBufferSize
|
||||
}
|
||||
newBuf := make([]byte, newLen)
|
||||
copy(newBuf, cc.buf[cc.start:cc.end])
|
||||
cc.buf = newBuf
|
||||
} else if cc.start != 0 {
|
||||
// slide remaining data back to the front of the buffer
|
||||
copy(cc.buf, cc.buf[cc.start:cc.end])
|
||||
}
|
||||
cc.end = cc.end - cc.start
|
||||
cc.start = 0
|
||||
|
||||
cc.searchFrom = cc.end
|
||||
n, err := cc.conn.Read(cc.buf[cc.end:])
|
||||
cc.end += n
|
||||
if n != 0 && err == io.EOF {
|
||||
// we may have received new \n-terminated lines, try to parse them
|
||||
cc.eof = true
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
line, err := cc.reader.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if globalUtf8EnforcementSetting && !utf8.Valid(line) {
|
||||
return line, errInvalidUtf8
|
||||
} else {
|
||||
return line, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,26 +93,36 @@ func (cc *IRCStreamConn) Close() (err error) {
|
||||
|
||||
// IRCWSConn is an IRCConn over a websocket.
|
||||
type IRCWSConn struct {
|
||||
conn *websocket.Conn
|
||||
conn *websocket.Conn
|
||||
buf []byte
|
||||
binary bool
|
||||
}
|
||||
|
||||
func NewIRCWSConn(conn *websocket.Conn) IRCWSConn {
|
||||
return IRCWSConn{conn: conn}
|
||||
func NewIRCWSConn(conn *websocket.Conn) *IRCWSConn {
|
||||
return &IRCWSConn{
|
||||
conn: conn,
|
||||
binary: conn.Subprotocol() == "binary.ircv3.net",
|
||||
buf: make([]byte, initialBufferSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (wc IRCWSConn) UnderlyingConn() *utils.WrappedConn {
|
||||
func (wc *IRCWSConn) UnderlyingConn() *utils.WrappedConn {
|
||||
// just assume that the type is OK
|
||||
wConn, _ := wc.conn.UnderlyingConn().(*utils.WrappedConn)
|
||||
return wConn
|
||||
}
|
||||
|
||||
func (wc IRCWSConn) WriteLine(buf []byte) (err error) {
|
||||
func (wc *IRCWSConn) WriteLine(buf []byte) (err error) {
|
||||
buf = bytes.TrimSuffix(buf, crlf)
|
||||
// #1483: if we have websockets at all, then we're enforcing utf8
|
||||
return wc.conn.WriteMessage(websocket.TextMessage, buf)
|
||||
messageType := websocket.TextMessage
|
||||
if wc.binary {
|
||||
messageType = websocket.BinaryMessage
|
||||
}
|
||||
return wc.conn.WriteMessage(messageType, buf)
|
||||
}
|
||||
|
||||
func (wc IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
||||
func (wc *IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
||||
for _, buf := range buffers {
|
||||
err = wc.WriteLine(buf)
|
||||
if err != nil {
|
||||
@ -166,21 +132,47 @@ func (wc IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (wc IRCWSConn) ReadLine() (line []byte, err error) {
|
||||
messageType, line, err := wc.conn.ReadMessage()
|
||||
if err == nil {
|
||||
if messageType == websocket.TextMessage {
|
||||
return line, nil
|
||||
} else {
|
||||
return nil, errWSBinaryMessage
|
||||
func (wc *IRCWSConn) ReadLine() (line []byte, err error) {
|
||||
_, reader, err := wc.conn.NextReader()
|
||||
switch err {
|
||||
case nil:
|
||||
// OK
|
||||
case websocket.ErrReadLimit:
|
||||
return line, ircreader.ErrReadQ
|
||||
default:
|
||||
return line, err
|
||||
}
|
||||
|
||||
line, err = wc.readFull(reader)
|
||||
switch err {
|
||||
case io.ErrUnexpectedEOF, io.EOF:
|
||||
// these are OK. io.ErrUnexpectedEOF is the good case:
|
||||
// it means we read the full message and it consumed less than the full wc.buf
|
||||
if !utf8.Valid(line) {
|
||||
return line, errInvalidUtf8
|
||||
}
|
||||
} else if err == websocket.ErrReadLimit {
|
||||
return line, errReadQ
|
||||
} else {
|
||||
return line, nil
|
||||
case nil, websocket.ErrReadLimit:
|
||||
// nil means we filled wc.buf without exhausting the reader:
|
||||
return line, ircreader.ErrReadQ
|
||||
default:
|
||||
return line, err
|
||||
}
|
||||
}
|
||||
|
||||
func (wc IRCWSConn) Close() (err error) {
|
||||
func (wc *IRCWSConn) readFull(reader io.Reader) (line []byte, err error) {
|
||||
// XXX this is io.ReadFull with a single attempt to resize upwards
|
||||
n, err := io.ReadFull(reader, wc.buf)
|
||||
if err == nil && len(wc.buf) < maxReadQBytes() {
|
||||
newBuf := make([]byte, maxReadQBytes())
|
||||
copy(newBuf, wc.buf[:n])
|
||||
wc.buf = newBuf
|
||||
n2, err := io.ReadFull(reader, wc.buf[n:])
|
||||
return wc.buf[:n+n2], err
|
||||
}
|
||||
return wc.buf[:n], err
|
||||
}
|
||||
|
||||
func (wc *IRCWSConn) Close() (err error) {
|
||||
return wc.conn.Close()
|
||||
}
|
||||
|
||||
@ -1,135 +0,0 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
// mockConn is a fake net.Conn / io.Reader that yields len(counts) lines,
|
||||
// each consisting of counts[i] 'a' characters and a terminating '\n'
|
||||
type mockConn struct {
|
||||
counts []int
|
||||
}
|
||||
|
||||
func min(i, j int) (m int) {
|
||||
if i < j {
|
||||
return i
|
||||
} else {
|
||||
return j
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mockConn) Read(b []byte) (n int, err error) {
|
||||
for len(b) > 0 {
|
||||
if len(c.counts) == 0 {
|
||||
return n, io.EOF
|
||||
}
|
||||
if c.counts[0] == 0 {
|
||||
b[0] = '\n'
|
||||
c.counts = c.counts[1:]
|
||||
b = b[1:]
|
||||
n += 1
|
||||
continue
|
||||
}
|
||||
size := min(c.counts[0], len(b))
|
||||
for i := 0; i < size; i++ {
|
||||
b[i] = 'a'
|
||||
}
|
||||
c.counts[0] -= size
|
||||
b = b[size:]
|
||||
n += size
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *mockConn) Write(b []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *mockConn) Close() error {
|
||||
c.counts = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockConn) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockConn) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockConn) SetDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockConn) SetReadDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMockConn(counts []int) *utils.WrappedConn {
|
||||
cpCounts := make([]int, len(counts))
|
||||
copy(cpCounts, counts)
|
||||
c := &mockConn{
|
||||
counts: cpCounts,
|
||||
}
|
||||
return &utils.WrappedConn{
|
||||
Conn: c,
|
||||
}
|
||||
}
|
||||
|
||||
// construct a mock reader with some number of \n-terminated lines,
|
||||
// verify that IRCStreamConn can read and split them as expected
|
||||
func doLineReaderTest(counts []int, t *testing.T) {
|
||||
c := newMockConn(counts)
|
||||
r := NewIRCStreamConn(c)
|
||||
var readCounts []int
|
||||
for {
|
||||
line, err := r.ReadLine()
|
||||
if err == nil {
|
||||
readCounts = append(readCounts, len(line))
|
||||
} else if err == io.EOF {
|
||||
break
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(counts, readCounts) {
|
||||
t.Errorf("expected %#v, got %#v", counts, readCounts)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
maxMockReaderLen = 100
|
||||
maxMockReaderLineLen = 4096 + 511
|
||||
)
|
||||
|
||||
func TestLineReader(t *testing.T) {
|
||||
counts := []int{44, 428, 3, 0, 200, 2000, 0, 4044, 33, 3, 2, 1, 0, 1, 2, 3, 48, 555}
|
||||
doLineReaderTest(counts, t)
|
||||
|
||||
// fuzz
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < 1000; i++ {
|
||||
countsLen := r.Intn(maxMockReaderLen) + 1
|
||||
counts := make([]int, countsLen)
|
||||
for i := 0; i < countsLen; i++ {
|
||||
counts[i] = r.Intn(maxMockReaderLineLen)
|
||||
}
|
||||
doLineReaderTest(counts, t)
|
||||
}
|
||||
}
|
||||
@ -5,12 +5,18 @@ package isupport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
maxLastArgLength = 400
|
||||
maxPayloadLength = 380
|
||||
|
||||
/* Modern: "As the maximum number of message parameters to any reply is 15,
|
||||
the maximum number of RPL_ISUPPORT tokens that can be advertised is 13."
|
||||
<nickname> [up to 13 parameters] <human-readable trailing>
|
||||
*/
|
||||
maxParameters = 13
|
||||
)
|
||||
|
||||
// List holds a list of ISUPPORT tokens
|
||||
@ -41,6 +47,12 @@ func (il *List) AddNoValue(name string) {
|
||||
il.Tokens[name] = ""
|
||||
}
|
||||
|
||||
// Contains returns whether the list already contains a token
|
||||
func (il *List) Contains(name string) bool {
|
||||
_, ok := il.Tokens[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// getTokenString gets the appropriate string for a token+value.
|
||||
func getTokenString(name string, value string) string {
|
||||
if len(value) == 0 {
|
||||
@ -52,7 +64,7 @@ func getTokenString(name string, value string) string {
|
||||
|
||||
// GetDifference returns the difference between two token lists.
|
||||
func (il *List) GetDifference(newil *List) [][]string {
|
||||
var outTokens sort.StringSlice
|
||||
var outTokens []string
|
||||
|
||||
// append removed tokens
|
||||
for name := range il.Tokens {
|
||||
@ -78,7 +90,7 @@ func (il *List) GetDifference(newil *List) [][]string {
|
||||
outTokens = append(outTokens, token)
|
||||
}
|
||||
|
||||
sort.Sort(outTokens)
|
||||
slices.Sort(outTokens)
|
||||
|
||||
// create output list
|
||||
replies := make([][]string, 0)
|
||||
@ -86,7 +98,7 @@ func (il *List) GetDifference(newil *List) [][]string {
|
||||
var cache []string // Token list cache
|
||||
|
||||
for _, token := range outTokens {
|
||||
if len(token)+length <= maxLastArgLength {
|
||||
if len(token)+length <= maxPayloadLength {
|
||||
// account for the space separating tokens
|
||||
if len(cache) > 0 {
|
||||
length++
|
||||
@ -95,7 +107,7 @@ func (il *List) GetDifference(newil *List) [][]string {
|
||||
length += len(token)
|
||||
}
|
||||
|
||||
if len(cache) == 13 || len(token)+length >= maxLastArgLength {
|
||||
if len(cache) == maxParameters || len(token)+length >= maxPayloadLength {
|
||||
replies = append(replies, cache)
|
||||
cache = make([]string, 0)
|
||||
length = 0
|
||||
@ -109,40 +121,54 @@ func (il *List) GetDifference(newil *List) [][]string {
|
||||
return replies
|
||||
}
|
||||
|
||||
func validateToken(token string) error {
|
||||
if len(token) == 0 || token[0] == ':' || strings.Contains(token, " ") {
|
||||
return fmt.Errorf("bad isupport token (cannot be sent as IRC parameter): `%s`", token)
|
||||
}
|
||||
|
||||
if strings.ContainsAny(token, "\n\r\x00") {
|
||||
return fmt.Errorf("bad isupport token (contains forbidden octets)")
|
||||
}
|
||||
|
||||
// technically a token can be maxPayloadLength if it occurs alone,
|
||||
// but fail it just to be safe
|
||||
if len(token) >= maxPayloadLength {
|
||||
return fmt.Errorf("bad isupport token (too long): `%s`", token)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegenerateCachedReply regenerates the cached RPL_ISUPPORT reply
|
||||
func (il *List) RegenerateCachedReply() (err error) {
|
||||
il.CachedReply = make([][]string, 0)
|
||||
var length int // Length of the current cache
|
||||
var cache []string // Token list cache
|
||||
|
||||
// make sure we get a sorted list of tokens, needed for tests and looks nice
|
||||
var tokens sort.StringSlice
|
||||
for name := range il.Tokens {
|
||||
tokens = append(tokens, name)
|
||||
var tokens []string
|
||||
for name, value := range il.Tokens {
|
||||
token := getTokenString(name, value)
|
||||
if tokenErr := validateToken(token); tokenErr == nil {
|
||||
tokens = append(tokens, token)
|
||||
} else {
|
||||
err = tokenErr
|
||||
}
|
||||
}
|
||||
sort.Sort(tokens)
|
||||
// make sure we get a sorted list of tokens, needed for tests and looks nice
|
||||
slices.Sort(tokens)
|
||||
|
||||
for _, name := range tokens {
|
||||
token := getTokenString(name, il.Tokens[name])
|
||||
if token[0] == ':' || strings.Contains(token, " ") {
|
||||
err = fmt.Errorf("bad isupport token (cannot contain spaces or start with :): %s", token)
|
||||
continue
|
||||
}
|
||||
var cache []string // Tokens in current line
|
||||
var length int // Length of the current line
|
||||
|
||||
if len(token)+length <= maxLastArgLength {
|
||||
// account for the space separating tokens
|
||||
if len(cache) > 0 {
|
||||
length++
|
||||
}
|
||||
cache = append(cache, token)
|
||||
length += len(token)
|
||||
}
|
||||
|
||||
if len(cache) == 13 || len(token)+length >= maxLastArgLength {
|
||||
for _, token := range tokens {
|
||||
// account for the space separating tokens
|
||||
if len(cache) == maxParameters || (len(token)+1)+length > maxPayloadLength {
|
||||
il.CachedReply = append(il.CachedReply, cache)
|
||||
cache = make([]string, 0)
|
||||
cache = nil
|
||||
length = 0
|
||||
}
|
||||
|
||||
if len(cache) > 0 {
|
||||
length++
|
||||
}
|
||||
length += len(token)
|
||||
cache = append(cache, token)
|
||||
}
|
||||
|
||||
if len(cache) > 0 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user