mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 17:14:02 +03:00
REPOSITORY / ScuroNeko/mtg
Compare commits
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4814b0fcc1 | ||
|
|
3ce549bb16 | ||
|
|
5c636a68dc | ||
|
|
9f6f906786 | ||
|
|
eb32766c1f | ||
|
|
787d72cf52 | ||
|
|
ce8f1ebb6c | ||
|
|
cd29f3e20b | ||
|
|
3e105f2beb | ||
|
|
6bbdd99e7f | ||
|
|
894c68019e | ||
|
|
98f18fc22b | ||
|
|
01b739aa7c | ||
|
|
dbaa743e03 | ||
|
|
d1e5f9d145 | ||
|
|
686f177ab9 | ||
|
|
706aef8ca1 | ||
|
|
14dfb9506a | ||
|
|
f742066c54 | ||
|
|
fbe4d32590 | ||
|
|
d0f18be91d | ||
|
|
929b73e2eb | ||
|
|
6b7364238a | ||
|
|
866906e770 | ||
|
|
b3c8c4a47d | ||
|
|
fbc7499cda | ||
|
|
ca5800cf60 | ||
|
|
00bb7be900 | ||
|
|
e8c70603f7 | ||
|
|
9d72904508 | ||
|
|
456ed5b051 | ||
|
|
4b7be8c565 | ||
|
|
8758208de6 | ||
|
|
6cd91600b5 | ||
|
|
bd39fa63cb | ||
|
|
eaa757b6d0 | ||
|
|
dc99dd165e | ||
|
|
7c70fd8079 | ||
|
|
dcbbb49607 | ||
|
|
1050ca0b97 | ||
|
|
c85c88efd6 | ||
|
|
c53364d952 | ||
|
|
ed91290e47 | ||
|
|
3fd5e9eb19 | ||
|
|
87ed1d1aa7 | ||
|
|
ec4f0656fb | ||
|
|
0c5d487fae | ||
|
|
889ab6c227 | ||
|
|
e1864377c2 | ||
|
|
4c8925ab65 | ||
|
|
bf34f0c92d | ||
|
|
604b1d2597 | ||
|
|
b19f491fc8 | ||
|
|
16f9ec690b | ||
|
|
fec60083a3 | ||
|
|
0882d45d6a | ||
|
|
86c6541e0d | ||
|
|
cc1f60c4a6 | ||
|
|
700f71bb31 | ||
|
|
097b52c8ef | ||
|
|
c3ffd7a307 | ||
|
|
fe3b0f3ca5 | ||
|
|
2273a0e4af | ||
|
|
115510985a | ||
|
|
2b5b325722 | ||
|
|
c536cbecd5 | ||
|
|
5e9d0649a2 | ||
|
|
585ebfeb50 | ||
|
|
81bca75c25 | ||
|
|
4f048a3b82 | ||
|
|
43f5d2131e | ||
|
|
e2073f0585 | ||
|
|
0652f322fa | ||
|
|
d3aa0f2de5 | ||
|
|
40c6cf294a | ||
|
|
4c38ea2b11 | ||
|
|
e6d444546f | ||
|
|
611583ba88 | ||
|
|
463af648ce | ||
|
|
b748603096 | ||
|
|
54a7c6a2a5 | ||
|
|
a3bae795c4 | ||
|
|
372c703ad6 | ||
|
|
0dd890a09d | ||
|
|
0274b3436a | ||
|
|
04b88cc864 | ||
|
|
b3112c5c67 | ||
|
|
3661fe108d | ||
|
|
5d5b77d73b | ||
|
|
7bffdcdf4a | ||
|
|
ea7033d569 | ||
|
|
24fdec6694 | ||
|
|
2fe04c7c9b | ||
|
|
7b5cb8e14b | ||
|
|
8566cede74 | ||
|
|
2dc30c5837 | ||
|
|
2cdff017e2 | ||
|
|
766800700a | ||
|
|
7e7823118e | ||
|
|
784a5818c8 | ||
|
|
75b95995a9 | ||
|
|
5941f0674a | ||
|
|
155a97c25d | ||
|
|
1fd0ebea9b | ||
|
|
988f61718c | ||
|
|
3d9a63a1f0 | ||
|
|
6c92ec314d | ||
|
|
132a01c27e | ||
|
|
b91f28d7d9 | ||
|
|
339840c14a | ||
|
|
a368975959 | ||
|
|
102e7610be | ||
|
|
e78c4d3bde | ||
|
|
1925efc2ec | ||
|
|
9562a86038 | ||
|
|
21d4f3d101 | ||
|
|
38ff62fddc | ||
|
|
26070d5b3e | ||
|
|
03d98ee256 | ||
|
|
dc28677086 | ||
|
|
54f4b397b6 | ||
|
|
96bcb6cd8e | ||
|
|
491b674e70 | ||
|
|
fdbee86d5e | ||
|
|
5b0d54e74d | ||
|
|
64754d1039 | ||
|
|
23c42def4f | ||
|
|
87a720dd58 | ||
|
|
10b78322a3 | ||
|
|
841a4d2227 | ||
|
|
3cacd74e12 | ||
|
|
71d4c6c42f | ||
|
|
75b05b9b14 | ||
|
|
daa8b9c798 | ||
|
|
bef14bd009 | ||
|
|
36d695118e | ||
|
|
4c3f42e264 | ||
|
|
3992054560 | ||
|
|
db8614999a | ||
|
|
bc2bd4510a | ||
|
|
5eca6ecb05 | ||
|
|
ce30e128e5 | ||
|
|
dedd67780b | ||
|
|
d8d954bbe8 | ||
|
|
bddf180575 | ||
|
|
9b78f766e8 | ||
|
|
e2e464d32c | ||
|
|
40b483357f | ||
|
|
f0efa4697e | ||
|
|
6d92d5fe21 | ||
|
|
c3e8e8b1fe | ||
|
|
a444a10bd0 | ||
|
|
d3551aa9cc | ||
|
|
4a2d1df384 | ||
|
|
a3c64c1d1e | ||
|
|
bbe8f34e72 | ||
|
|
02aa969d6c | ||
|
|
4da2a4f10f | ||
|
|
336e825e3a | ||
|
|
b42e51e300 | ||
|
|
6b1bfe7b17 | ||
|
|
6219f4bd90 | ||
|
|
3076c52adb | ||
|
|
933855a233 | ||
|
|
ea698259d5 | ||
|
|
aefa003355 | ||
|
|
e3b7472239 | ||
|
|
a3362c7ea4 | ||
|
|
66c45dc83b | ||
|
|
f7c33ee333 | ||
|
|
42160a08fe | ||
|
|
925a02dac3 | ||
|
|
7108fe99ec | ||
|
|
69203f3e23 | ||
|
|
4d2d21e101 | ||
|
|
8de727932d | ||
|
|
188fa6a04a | ||
|
|
0330a0e5cd | ||
|
|
0ad2d61742 | ||
|
|
f155b9f37e | ||
|
|
3a44bcb854 | ||
|
|
58335b3e59 | ||
|
|
657a74a5c2 | ||
|
|
f3112d4ba6 | ||
|
|
172b596cf1 | ||
|
|
b258581f47 | ||
|
|
60d5b3cadd | ||
|
|
f21ee40baf | ||
|
|
adf4ab1a35 | ||
|
|
83eeedc008 | ||
|
|
57cb1b5aa0 | ||
|
|
5e31b95bb5 | ||
|
|
80a1de60e6 | ||
|
|
2408f1530f | ||
|
|
23519913f2 | ||
|
|
7955ac6a46 | ||
|
|
dbbfdaf918 | ||
|
|
8e7207d975 | ||
|
|
c04f9e392a | ||
|
|
46bd617581 | ||
|
|
7617aeadda | ||
|
|
e009d05a90 | ||
|
|
f0063ba089 | ||
|
|
b08d945d7c | ||
|
|
abff0cf211 | ||
|
|
f52b3391d1 | ||
|
|
cae33a22e6 | ||
|
|
3dc263d6d7 | ||
|
|
8eb5fed92f | ||
|
|
ab1b3b4863 | ||
|
|
ab585fd699 | ||
|
|
91a1ee956c | ||
|
|
f8ad90c845 | ||
|
|
37a78bd1c3 | ||
|
|
18e3ca77de | ||
|
|
185baf6bc9 | ||
|
|
546a5849f3 | ||
|
|
2cc81219b1 | ||
|
|
1f862027af | ||
|
|
02643fb6bb | ||
|
|
e167b85d3b | ||
|
|
c0b6124d94 | ||
|
|
113b5ecbe0 | ||
|
|
4daf927a72 | ||
|
|
1218f18af9 | ||
|
|
7ca143d352 | ||
|
|
428010880e | ||
|
|
dc81740bda | ||
|
|
f7dc54b652 | ||
|
|
c0ab254acf | ||
|
|
5c4536c591 | ||
|
|
d48e82be7c | ||
|
|
bc4b14c83d | ||
|
|
6b9b437a5a | ||
|
|
d6566e5dfb | ||
|
|
6b28488fbd | ||
|
|
1a02511afe | ||
|
|
757ea5b63c | ||
|
|
24add0dce4 | ||
|
|
299c6478c2 | ||
|
|
ddd3d608fa | ||
|
|
d5b0a47d52 | ||
|
|
ca0a550f12 | ||
|
|
94084674c0 | ||
|
|
9aa56191f9 | ||
|
|
a34949f15c | ||
|
|
cbcc113e41 | ||
|
|
6f0b0e0c32 | ||
|
|
098a9c411a | ||
|
|
d1dd56550f | ||
|
|
59f328b804 | ||
|
|
783c49db37 | ||
|
|
015f02c077 | ||
|
|
e0833e86d9 | ||
|
|
f89f5f8469 | ||
|
|
0395ce8d1a | ||
|
|
ef87d031ed | ||
|
|
ba875da9b5 | ||
|
|
8f7e79862b | ||
|
|
9124ffabf1 | ||
|
|
c8914f90db | ||
|
|
7c43a4b0b7 | ||
|
|
e386ae0daf | ||
|
|
b5346668f8 | ||
|
|
7002e4cd09 | ||
|
|
ecfd550a95 | ||
|
|
5b46152945 | ||
|
|
88e076a283 | ||
|
|
0ed7e10379 | ||
|
|
21ee1e2c6c | ||
|
|
09f8233bc6 | ||
|
|
d5147f1935 | ||
|
|
4689479745 | ||
|
|
2be900745f | ||
|
|
baee322cd7 | ||
|
|
28fe63dbdb | ||
|
|
7718f62477 | ||
|
|
df0994dad3 | ||
|
|
dca43853fb | ||
|
|
ea8e31346c | ||
|
|
4aab3b686c | ||
|
|
ed495b3800 | ||
|
|
4025f223c4 | ||
|
|
121200cfad | ||
|
|
90e0ce4c48 | ||
|
|
ac0b442b43 | ||
|
|
af7021fac8 | ||
|
|
5468625212 | ||
|
|
345c1bf715 | ||
|
|
52d47a9786 | ||
|
|
2ba79a8508 | ||
|
|
ce7769472d | ||
|
|
eb5eea2625 | ||
|
|
d965dc3c07 | ||
|
|
040b3c371c | ||
|
|
c7ba8e54be | ||
|
|
33b1a5426c | ||
|
|
91197c0334 | ||
|
|
e075169dd4 | ||
|
|
225444b1b3 | ||
|
|
586ac9dcb9 | ||
|
|
c3be5311e8 | ||
|
|
a8c4d0a9c4 | ||
|
|
bfeded67ee | ||
|
|
dde0d25b77 | ||
|
|
296a34cd6e | ||
|
|
44cea68e83 | ||
|
|
790bf21166 | ||
|
|
5d17e264e6 | ||
|
|
7ef1e99f66 | ||
|
|
3acbacbdb3 | ||
|
|
5fd7062972 | ||
|
|
7a6695f6c4 | ||
|
|
8ac786581b | ||
|
|
86a8f69273 | ||
|
|
95b16cd805 | ||
|
|
3996b9a6b8 | ||
|
|
e286dea650 | ||
|
|
5e27e48129 | ||
|
|
95abb54dc8 | ||
|
|
dbd3d3c266 | ||
|
|
e5b9c841cd | ||
|
|
ad7acee64d | ||
|
|
9ce6674070 | ||
|
|
25edb60f2c | ||
|
|
b5a8d8b804 | ||
|
|
8ccf61c23e | ||
|
|
35f0fca3c5 | ||
|
|
cd46a734ea | ||
|
|
cd92a343aa | ||
|
|
289e8f5945 | ||
|
|
850bed0dfc | ||
|
|
8977d77a13 | ||
|
|
91726d461f | ||
|
|
be1a47287a | ||
|
|
426483d27b | ||
|
|
bc72717582 | ||
|
|
391d901402 | ||
|
|
837d96dc43 | ||
|
|
9125a29e79 | ||
|
|
6449109b2b | ||
|
|
829669eefd | ||
|
|
2d9f2345dc | ||
|
|
6a9069ca72 | ||
|
|
d72ed03a43 | ||
|
|
f0d2cd494a | ||
|
|
b5810c955d | ||
|
|
4ebae693e0 | ||
|
|
0e033b59b3 | ||
|
|
1a619e3090 | ||
|
|
8e29ff78ba | ||
|
|
efdac2c0d8 | ||
|
|
e559388802 | ||
|
|
cb691453f0 | ||
|
|
020065f389 | ||
|
|
cf5197cee0 | ||
|
|
720158f15c | ||
|
|
07f00a363e | ||
|
|
d32ab34e60 | ||
|
|
cdd93bd25f | ||
|
|
5a52418394 | ||
|
|
cf43ae780b | ||
|
|
2407748ab0 | ||
|
|
a47edf08d0 | ||
|
|
566955b8b7 | ||
|
|
5cc07a54a2 | ||
|
|
ba2143a6ae | ||
|
|
acb02baa2e | ||
|
|
422ed5b071 | ||
|
|
19efcc93fc | ||
|
|
b4c47ff4e1 | ||
|
|
8a783d6b4e | ||
|
|
c5029c706c | ||
|
|
d7e565e0f5 | ||
|
|
93686377d5 | ||
|
|
87de28636a | ||
|
|
7788b169b6 | ||
|
|
6b9601fc2c | ||
|
|
a65cb05d20 | ||
|
|
31866d5f64 | ||
|
|
0db83a0cc8 | ||
|
|
40bb59faf5 | ||
|
|
cac476901d | ||
|
|
307910fe4a | ||
|
|
bb21bf814f | ||
|
|
4c2c468289 | ||
|
|
f2f7c2a4de | ||
|
|
8fdd4292b1 | ||
|
|
44d339d777 | ||
|
|
c6223e256d | ||
|
|
7928c2b323 | ||
|
|
b2e7e1a225 | ||
|
|
843fc4b70f | ||
|
|
68e1d66017 | ||
|
|
ed9689d813 | ||
|
|
59ddffa911 | ||
|
|
3af9ede1c1 | ||
|
|
47364e7ba8 | ||
|
|
b6251608d9 | ||
|
|
6dce3944e4 | ||
|
|
112d3a916c | ||
|
|
5d3824704c | ||
|
|
0ff5ca6808 | ||
|
|
8e0436e89a | ||
|
|
646294b6ba | ||
|
|
196ef3d449 | ||
|
|
4f8755879d | ||
|
|
3451832d13 | ||
|
|
dfa53af567 | ||
|
|
27c5646680 | ||
|
|
596fafe30d | ||
|
|
e465817011 | ||
|
|
ef60877cb0 | ||
|
|
8b802819ee | ||
|
|
3c75fcca80 | ||
|
|
1258f07197 | ||
|
|
24137b6ea2 | ||
|
|
7d1d9007af | ||
|
|
ae479adeaa | ||
|
|
6732e80d06 | ||
|
|
2bf291bab5 | ||
|
|
273aa18367 | ||
|
|
b491e03504 | ||
|
|
a7a4da8ee5 | ||
|
|
34ad883559 | ||
|
|
22905b2a25 | ||
|
|
0ff9a58780 | ||
|
|
ed180ecb3d | ||
|
|
f005592872 | ||
|
|
d4cd779c42 | ||
|
|
9e3683ce08 | ||
|
|
d8216f776d | ||
|
|
038b2b200d | ||
|
|
fd8506c82a | ||
|
|
559a1a666b | ||
|
|
4c5f1a5636 | ||
|
|
fbbf9b1634 | ||
|
|
1e428e13fc | ||
|
|
f531d90fc4 | ||
|
|
5f168d1ee4 | ||
|
|
5009859a1e | ||
|
|
432e2970a0 | ||
|
|
6e154b3f44 | ||
|
|
56cf90b13d | ||
|
|
d10729490d | ||
|
|
d459efcf9c | ||
|
|
2eba78b0db | ||
|
|
61450ea3db | ||
|
|
c62afbdfd8 | ||
|
|
828627b0d8 | ||
|
|
c68f2cd50f | ||
|
|
e81c5970d4 | ||
|
|
1a7eee444e | ||
|
|
d9bd07b027 | ||
|
|
3ae69183aa | ||
|
|
6c7edfb7db | ||
|
|
d44474012a | ||
|
|
413cafeeb6 | ||
|
|
c9743b5675 | ||
|
|
072bce2922 | ||
|
|
386522ba22 | ||
|
|
029245cd16 | ||
|
|
b280f47731 | ||
|
|
1617866c24 | ||
|
|
691954bd16 | ||
|
|
9534cb19d0 | ||
|
|
7827d1255a | ||
|
|
d431feb0ba | ||
|
|
3816dbf5b1 | ||
|
|
2918ed11e5 | ||
|
|
701f62ed4c | ||
|
|
299a34252e | ||
|
|
2492a47d0a | ||
|
|
c2b8e88961 | ||
|
|
c9d3020ea6 | ||
|
|
018e5b96f4 | ||
|
|
07985cf418 | ||
|
|
389f6fd4bf | ||
|
|
09c7ce45d2 | ||
|
|
199eed0f18 | ||
|
|
82ec3871a4 | ||
|
|
2748fbd1e5 | ||
|
|
f14f104bea | ||
|
|
b1074193a8 | ||
|
|
78dea9ae3f | ||
|
|
da8dba1585 | ||
|
|
33852ca481 | ||
|
|
eff53694a0 | ||
|
|
74a7c505eb | ||
|
|
9983f7fcba | ||
|
|
a90072260e | ||
|
|
b153240bc3 | ||
|
|
6dfbd26524 | ||
|
|
39e7663e0c | ||
|
|
71f7bf6cad | ||
|
|
fba0e235c1 | ||
|
|
70be70de3c | ||
|
|
b565d83d40 | ||
|
|
ebc459440c | ||
|
|
6e6049a73a | ||
|
|
916714a909 | ||
|
|
f68b195306 | ||
|
|
5f1d1ce883 | ||
|
|
16558a7c55 | ||
|
|
e7958aaf33 | ||
|
|
7d6b661d97 | ||
|
|
28b3cbe91a | ||
|
|
6818364231 | ||
|
|
ea97bf51c8 | ||
|
|
1678ddbd53 | ||
|
|
f81f29cbb4 | ||
|
|
2d9259db48 | ||
|
|
c721c636d9 | ||
|
|
60472072aa | ||
|
|
6721e6fd9f | ||
|
|
47fb5c23cb | ||
|
|
fce6118c71 | ||
|
|
d277a2975a | ||
|
|
c3f21a7b0d | ||
|
|
61a1024264 | ||
|
|
4695a0c433 | ||
|
|
a9d0ca1e90 | ||
|
|
9346c11f37 | ||
|
|
5f00363da5 | ||
|
|
3aec9657d7 | ||
|
|
358d42ec61 | ||
|
|
ac2625209b | ||
|
|
c74e0ee359 | ||
|
|
7c463851b2 | ||
|
|
c9f6b922c3 | ||
|
|
7128e70e50 | ||
|
|
cf9bf56d8e | ||
|
|
76cfbb009a | ||
|
|
cb78169d6a | ||
|
|
d1703873c1 | ||
|
|
ac33abbbb1 | ||
|
|
a6893c8df7 | ||
|
|
7182c7bf65 | ||
|
|
84f4a37d11 | ||
|
|
3cf7f1396b | ||
|
|
835ab94470 | ||
|
|
278753d91e | ||
|
|
b81505ec21 | ||
|
|
0828bd85ad | ||
|
|
96e80fc0d6 | ||
|
|
591500de41 | ||
|
|
07ddfa12f7 | ||
|
|
618d91b15d | ||
|
|
a83d20dd46 | ||
|
|
1e9bbfae55 | ||
|
|
8268620615 | ||
|
|
753639beb8 | ||
|
|
1ee34fd502 | ||
|
|
243a89a68c | ||
|
|
9f20e8749a | ||
|
|
b0d86abc74 | ||
|
|
6c0030d20e | ||
|
|
8733f6d191 | ||
|
|
8906ae0db0 | ||
|
|
2e8c9623de | ||
|
|
0ce325f908 | ||
|
|
b86ad07ba9 | ||
|
|
6253c21615 | ||
|
|
a00408562f | ||
|
|
0d0bdd9fde | ||
|
|
c8da08ecce | ||
|
|
c5f99d17b7 | ||
|
|
c1b862c1e9 |
@@ -0,0 +1,4 @@
|
||||
---
|
||||
|
||||
fixes:
|
||||
- "github.com/9seconds/mtg/v2/::"
|
||||
@@ -0,0 +1,9 @@
|
||||
vendor/
|
||||
tags
|
||||
.github/
|
||||
.bin/
|
||||
*.md
|
||||
mtg
|
||||
.golangci.toml
|
||||
.gitignore
|
||||
run.sh
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
|
||||
name: CI
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- v1
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- released
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
strategy:
|
||||
matrix:
|
||||
go_version:
|
||||
- ~1.16
|
||||
- ^1.17
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go_version }}
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
|
||||
- name: Run tests
|
||||
run: go test -coverprofile=./coverage.txt -covermode=atomic -v ./...
|
||||
|
||||
- name: Collect coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Run linter
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.42.1
|
||||
|
||||
docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
with:
|
||||
images: nineseconds/mtg,ghcr.io/9seconds/mtg
|
||||
tag-semver: "{{version}},{{major}},{{major}}.{{minor}}"
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Setup BuildX
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR.io
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
pull: true
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/buildx-cache
|
||||
cache-to: type=local,dest=/tmp/buildx-cache
|
||||
@@ -0,0 +1,77 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions
|
||||
# https://github.com/github/codeql-action/issues/572
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
security-events: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '24 20 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
+3
-1
@@ -7,5 +7,7 @@
|
||||
*.out
|
||||
mtg
|
||||
vendor/
|
||||
version.go
|
||||
ccbuilds/
|
||||
.bin/
|
||||
coverage.txt
|
||||
dist/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[run]
|
||||
concurrency = 4
|
||||
deadline = "2m"
|
||||
tests = true
|
||||
skip-dirs = ["vendor"]
|
||||
|
||||
[output]
|
||||
format = "colored-line-number"
|
||||
|
||||
[linters]
|
||||
enable-all = true
|
||||
disable = ["gochecknoglobals", "gas", "goerr113", "exhaustivestruct"]
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"aggregate": true,
|
||||
"vendor": true
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
|
||||
project_name: mtg
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- binary: '{{ .ProjectName }}'
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
- -mod=readonly
|
||||
ldflags: -s -w -X main.version={{ .Version }}
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
- goos: netbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
|
||||
archives:
|
||||
- name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||
format: tar.gz
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- SECURITY.md
|
||||
|
||||
gomod:
|
||||
proxy: true
|
||||
|
||||
snapshot:
|
||||
name_template: '{{ .Version }}'
|
||||
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
name_template: '{{ .ProjectName }}-sources'
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
---
|
||||
|
||||
language: go
|
||||
sudo: false
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- "1.9.x"
|
||||
- "1.10.x"
|
||||
- master
|
||||
|
||||
script:
|
||||
- make lint
|
||||
- make test
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
+10
-17
@@ -1,7 +1,7 @@
|
||||
###############################################################################
|
||||
# BUILD STAGE
|
||||
|
||||
FROM golang:alpine
|
||||
FROM golang:1.17-alpine AS build
|
||||
|
||||
RUN set -x \
|
||||
&& apk --no-cache --update add \
|
||||
@@ -9,17 +9,13 @@ RUN set -x \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
make \
|
||||
upx \
|
||||
&& update-ca-certificates
|
||||
make
|
||||
|
||||
ADD . /go/src/github.com/9seconds/mtg
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN set -x \
|
||||
&& cd /go/src/github.com/9seconds/mtg \
|
||||
&& make clean \
|
||||
&& make -j 4 static \
|
||||
&& upx --ultra-brute -qq ./mtg
|
||||
&& make -j 4 static
|
||||
|
||||
|
||||
###############################################################################
|
||||
@@ -27,12 +23,9 @@ RUN set -x \
|
||||
|
||||
FROM scratch
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/mtg"]
|
||||
ENV MTG_IP=0.0.0.0 \
|
||||
MTG_PORT=3128 \
|
||||
MTG_STATS_IP=0.0.0.0 \
|
||||
MTG_STATS_PORT=3129
|
||||
EXPOSE 3128 3129
|
||||
ENTRYPOINT ["/mtg"]
|
||||
CMD ["run", "/config.toml"]
|
||||
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=0 /go/src/github.com/9seconds/mtg/mtg /usr/local/bin/mtg
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=build /app/mtg /mtg
|
||||
COPY --from=build /app/example.config.toml /config.toml
|
||||
|
||||
Generated
-114
@@ -1,114 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [
|
||||
".",
|
||||
"parse"
|
||||
]
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/units"
|
||||
packages = ["."]
|
||||
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/beevik/ntp"
|
||||
packages = ["."]
|
||||
revision = "62c80a04de2086884d8296004b6d74ee1846c582"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/juju/errors"
|
||||
packages = ["."]
|
||||
revision = "c7d06af17c68cd34c835053720b21f6549d9b0ee"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"internal/bufferpool",
|
||||
"internal/color",
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"ipv4"
|
||||
]
|
||||
revision = "d0887baf81f4598189d4e12a37c6da86f0bba4d0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alexcesaro/statsd.v2"
|
||||
packages = ["."]
|
||||
revision = "7fea3f0d2fab1ad973e641e51dba45443a311a90"
|
||||
version = "v2.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "7fad0f62feb7737b064d85cc4333a1a3e9298faec2afd864b4404f515fc7f17c"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
version = "2.2.6"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/juju/errors"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/beevik/ntp"
|
||||
version = "0.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/alexcesaro/statsd.v2"
|
||||
version = "2.0.0"
|
||||
@@ -1,61 +1,53 @@
|
||||
ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
IMAGE_NAME := mtg
|
||||
APP_NAME := $(IMAGE_NAME)
|
||||
GOMETALINTER := gometalinter
|
||||
|
||||
VENDOR_FILES := $(shell find "$(ROOT_DIR)/vendor" 2>/dev/null || echo -n "vendor")
|
||||
CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}")
|
||||
APP_DEPS := version.go $(VENDOR_FILES)
|
||||
GOLANGCI_LINT_VERSION := v1.42.1
|
||||
|
||||
COMMON_BUILD_FLAGS := -ldflags="-s -w"
|
||||
VERSION_GO := $(shell go version)
|
||||
VERSION_DATE := $(shell date -Ru)
|
||||
VERSION_TAG := $(shell git describe --tags --always)
|
||||
COMMON_BUILD_FLAGS := -trimpath -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
$(APP_NAME): $(APP_DEPS)
|
||||
@go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
static-$(APP_NAME): $(APP_DEPS)
|
||||
@env CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
$(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
|
||||
$(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
|
||||
$(APP_NAME)-%: $(APP_DEPS) ccbuilds
|
||||
@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
|
||||
go build \
|
||||
$(COMMON_BUILD_FLAGS) \
|
||||
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
|
||||
|
||||
ccbuilds:
|
||||
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
|
||||
|
||||
version.go:
|
||||
@go generate main.go
|
||||
|
||||
vendor: Gopkg.lock Gopkg.toml install-cli
|
||||
@dep ensure
|
||||
GOBIN := $(ROOT_DIR)/.bin
|
||||
GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
.PHONY: all
|
||||
all: $(APP_NAME)
|
||||
all: build
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
$(APP_NAME): build
|
||||
|
||||
.PHONY: static
|
||||
static: static-$(APP_NAME)
|
||||
static:
|
||||
@env CGO_ENABLED=0 GOOS=linux go build \
|
||||
$(COMMON_BUILD_FLAGS) \
|
||||
-tags netgo \
|
||||
-a \
|
||||
-o "$(APP_NAME)"
|
||||
|
||||
.PHONY: crosscompile
|
||||
crosscompile: $(CC_BINARIES)
|
||||
vendor: go.mod go.sum
|
||||
@$(MOD_ON) go mod vendor
|
||||
|
||||
.PHONY: crosscompile-dir
|
||||
crosscompile-dir:
|
||||
@rm -rf "$(CC_DIR)" && mkdir -p "$(CC_DIR)"
|
||||
.bin:
|
||||
@mkdir -p "$(GOBIN)" || true
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@$(GOTOOL) gofumpt -w -s -extra "$(ROOT_DIR)"
|
||||
|
||||
.PHONY: test
|
||||
test: vendor install-cli version.go
|
||||
test:
|
||||
@go test -v ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint: vendor install-cli version.go
|
||||
@$(GOMETALINTER) --deadline=2m ./...
|
||||
.PHONY: citest
|
||||
citest:
|
||||
@go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./...
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@@ -63,18 +55,44 @@ clean:
|
||||
git reset --hard >/dev/null && \
|
||||
git submodule foreach --recursive sh -c 'git clean -xfd && git reset --hard' >/dev/null
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@$(GOTOOL) golangci-lint run
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
@$(GOTOOL) goreleaser release --snapshot --rm-dist && \
|
||||
find "$(ROOT_DIR)/dist" -type d | grep -vP "dist$$" | xargs -r rm -rf && \
|
||||
rm -f "$(ROOT_DIR)/dist/config.yaml"
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
|
||||
|
||||
.PHONY: install-cli
|
||||
install-cli: install-dep install-lint
|
||||
.PHONY: doc
|
||||
doc:
|
||||
@$(GOTOOL) godoc -http 0.0.0.0:10000
|
||||
|
||||
.PHONY: install-dep
|
||||
install-dep:
|
||||
@go get github.com/golang/dep/cmd/dep
|
||||
.PHONY: install-tools
|
||||
install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt install-tools-goreleaser
|
||||
|
||||
.PHONY: install-lint
|
||||
install-lint:
|
||||
@go get github.com/alecthomas/gometalinter && \
|
||||
$(GOMETALINTER) --install >/dev/null
|
||||
.PHONY: install-tools-lint
|
||||
install-tools-lint: .bin
|
||||
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
|
||||
| bash -s -- -b "$(GOBIN)" "$(GOLANGCI_LINT_VERSION)"
|
||||
|
||||
.PHONY: install-tools-godoc
|
||||
install-tools-godoc: .bin
|
||||
@$(GOTOOL) go install golang.org/x/tools/cmd/godoc@latest
|
||||
|
||||
.PHONY: install-tools-gofumpt
|
||||
install-tools-gofumpt: .bin
|
||||
@$(GOTOOL) go install mvdan.cc/gofumpt@latest
|
||||
|
||||
.PHONY: goreleaser
|
||||
install-tools-goreleaser: .bin
|
||||
@$(GOTOOL) go install github.com/goreleaser/goreleaser@latest
|
||||
|
||||
.PHONY: update-deps
|
||||
update-deps:
|
||||
@go get -u && go mod tidy
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# mtg
|
||||
|
||||
Bullshit-free MTPROTO proxy for Telegram
|
||||
Highly-opionated (ex-bullshit-free) MTPROTO proxy for
|
||||
[Telegram](https://telegram.org/).
|
||||
|
||||
[](https://travis-ci.org/9seconds/mtg)
|
||||
[](https://hub.docker.com/r/nineseconds/mtg/)
|
||||
[](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
|
||||
[](https://codecov.io/gh/9seconds/mtg)
|
||||
[](https://pkg.go.dev/github.com/9seconds/mtg/v2)
|
||||
|
||||
**If you use v1.0 or upgrade broke you proxy, please read the chapter
|
||||
[Version 2](#version-2)**
|
||||
|
||||
# Rationale
|
||||
## Rationale
|
||||
|
||||
There are several available proxies for Telegram MTPROTO available. Here
|
||||
are the most notable:
|
||||
@@ -15,183 +19,366 @@ are the most notable:
|
||||
* [Python](https://github.com/alexbers/mtprotoproxy)
|
||||
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
||||
|
||||
Almost all of them follow the way how official proxy was build. This
|
||||
includes support of multiple secrets, support of promoted channels etc.
|
||||
You can use any of these. They work great and all implementations have
|
||||
feature parity now. This includes support of adtag, replay attack
|
||||
protection, domain fronting, faketls, and so on. mtg has a similar
|
||||
goal: to give a possibility to connect to Telegram in a restricted,
|
||||
censored environment. But it does it slightly differently in details
|
||||
that probably matter.
|
||||
|
||||
mtg is an implementation in golang which is intended to be:
|
||||
* **Resource-efficient**
|
||||
|
||||
It has to be resource-efficient. It does not mean that you will see
|
||||
the smallest memory usage. It means that it will try to use allocated
|
||||
resources in zero-waste mode, reusing as much memory as possible and
|
||||
so on.
|
||||
|
||||
* **Lightweight**
|
||||
It has to consume as less resources as possible but not by losing
|
||||
maintainability.
|
||||
* **Easily deployable**
|
||||
|
||||
I strongly believe that Telegram proxies should follow the way of
|
||||
ShadowSocks: promoted channels is a strange way of doing business
|
||||
I suppose. I think the only viable way is to have a proxy with
|
||||
minimum configuration which should work everywhere.
|
||||
* **Single secret**
|
||||
I think that multiple secrets solves no problems and just complexify
|
||||
software. I also believe that in case of throwout proxies, this feature
|
||||
is useless luxury.
|
||||
* **Minimum docker image size**
|
||||
Official image is less than 2.5 megabytes. Literally.
|
||||
[ShadowSocks](https://shadowsocks.org): promoted channels is a strange
|
||||
way of doing business I suppose. I think the only viable way is to
|
||||
have a proxy that can be restored anywhere easily.
|
||||
|
||||
* **A single secret**
|
||||
|
||||
I think that multiple secrets solve no problems and just complex
|
||||
software. I also believe that in the case of throwout proxies, this
|
||||
the feature is a useless luxury.
|
||||
|
||||
* **No adtag support**
|
||||
|
||||
Please read [Version 2](#version-2) chapter.
|
||||
|
||||
* **No management WebUI**
|
||||
This is an implementation of simple lightweight proxy. I won't do that.
|
||||
|
||||
This proxy supports 2 modes of work: direct connection to Telegram and
|
||||
promoted channel mode. If you do not need promoted channels, I would
|
||||
recommend you to go with direct mode: this is way more robust.
|
||||
This is an implementation of a simple lightweight proxy. I won't do that.
|
||||
|
||||
To run proxy in direct mode, all you need to do is just provide a
|
||||
secret. If you do not provide ADTag as a second parameter, promoted
|
||||
channels mode won't be activated.
|
||||
* **Proxy chaining**
|
||||
|
||||
To get promoted channel, please contact
|
||||
[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
|
||||
second parameter.
|
||||
mtg has the support of [SOCKS5](https://en.wikipedia.org/wiki/SOCKS)
|
||||
proxies. So, in theory, you can run this proxy as a frontend
|
||||
and route traffic via [v2ray](https://www.v2ray.com/),
|
||||
[Gost](https://docs.ginuerzh.xyz/gost/),
|
||||
[Trojan](https://trojan-gfw.github.io/trojan/), or any other project
|
||||
you like.
|
||||
|
||||
* **Native blocklist support**
|
||||
|
||||
# Source code organization
|
||||
Previously, this was delegated to the [FireHOL](https://firehol.org/)
|
||||
project or similar ones which track attacks and publish a list of
|
||||
potentially dangerous IPs. mtg has native support of such blocklists.
|
||||
|
||||
There are 2 main branches:
|
||||
* **Can be used as a library**
|
||||
|
||||
1. `master` branch contains potentially unstable features
|
||||
2. `stable` branch contains stable version. Usually you want to use this branch.
|
||||
mtg v2 was redesigned in a way so it can be embedded into your
|
||||
software (written in Golang) with a minimum effort + you can replace
|
||||
some parts with those you want.
|
||||
|
||||
# How to build
|
||||
### Version 2
|
||||
|
||||
If you use version 1.x before, you are probably noticed some major
|
||||
backward non-compatible details:
|
||||
|
||||
1. Configuration file
|
||||
2. Removed support of adtag
|
||||
|
||||
For the configuration file, please check out the full example in this
|
||||
repository. It has a lot of comments and most of the options are
|
||||
optional. We do have only `secret` and `bind-to` sections mandatory.
|
||||
Other sections in the example configuration file are filled with default
|
||||
values.
|
||||
|
||||
Adtag support was removed completely. This was done to debloat mtg and
|
||||
keep it simple and obvious. Hopefully, this goal is achieved and the
|
||||
source code is clean and straightforward enough.
|
||||
|
||||
I always was quite skeptical about adtag. In my POV, a proxy as a fat
|
||||
big connectivity point for hundreds of clients is an illusion. If you
|
||||
work in a censored environment, the first thing that authority does is
|
||||
IP blocking. For us, it means, those big proxies that can benefit from
|
||||
having a pinned channel are going to be blocked in a minute.
|
||||
|
||||
Proxy has to be intimate. It has to be shared within a small group as
|
||||
a family or maybe your college friends. It has to have a small number
|
||||
of connections and never publicly announced its presence. It has to fly
|
||||
under the radar. If the proxy is detected, you need to be able to give
|
||||
a rebirth on a new IP address as soon as possible. I do no think that
|
||||
having some special channel for such a use case makes any sense.
|
||||
|
||||
But other details like replay attack protection, domain fronting,
|
||||
accurate FakeTLS implementation, IP blacklisting, and proxy
|
||||
chaining matter here. If you work in censored perimeter like
|
||||
[GFW](https://en.wikipedia.org/wiki/Great_Firewall)-protected
|
||||
country, you probably want to have an MTPROTO proxy as
|
||||
a frontend that transports traffic via cloaked tunnels
|
||||
made by [Trojan](https://trojan-gfw.github.io/trojan/),
|
||||
[Shadowsocks](https://shadowsocks.org), [v2ray](https://www.v2ray.com/),
|
||||
or [Gost](https://docs.ginuerzh.xyz/gost/). That's why you have to have
|
||||
the support of chaining as a first-class citizen.
|
||||
|
||||
Yes, this is possible and doable with optional adtag support. But the
|
||||
truth is that the MTPROTO proxy for Telegram is just a thing that either
|
||||
work as a normal client (direct mode) or doing some RPC calls in [TL
|
||||
language](https://core.telegram.org/mtproto/TL) (adtag support). I
|
||||
understand the intention of the developers and I understand that they
|
||||
were under high pressure fighting with [RKN](https://rkn.gov.ru/) and
|
||||
doing TON after that. Nothing is ideal. But for the proxy, it means that
|
||||
source code is full of complex non-trivial code which is required only
|
||||
to support a feature that we barely need.
|
||||
|
||||
So, to have a reasonable MTPROTO proxy, adtag support was removed. This
|
||||
is a rare chance in my career where software v2 debloats a previous
|
||||
version. It feels so good :)
|
||||
|
||||
### Version 1 and 2
|
||||
|
||||
I do continue to support both versions 1 and 2. But in a different mode.
|
||||
|
||||
Version 1 is now officially in maintenance mode. It means that I won't
|
||||
make any new features or improvements there. You can consider a feature
|
||||
freeze there. No bugs are going to be fixed there except for critical
|
||||
ones. PRs are welcome though. The goal is to keep it working. It will
|
||||
get some periodical updates like updates to the new Golang version of
|
||||
dependencies version bump, but that's mostly it.
|
||||
|
||||
**If you want to have mtg with _adtag support_, please use version 1**.
|
||||
|
||||
Version 2 is going to have all my love, active support, bug fixing, etc.
|
||||
It is under active development and maintenance.
|
||||
|
||||
This project has several main branches
|
||||
|
||||
1. [`master`](https://github.com/9seconds/mtg/tree/master) branch
|
||||
contains a bleeding edge. It may potentially have some features
|
||||
which will break your source code.
|
||||
2. [`stable`](https://github.com/9seconds/mtg/tree/stable) branch contains
|
||||
dumps of a master branch when we consider it 'stable'. This is a
|
||||
branch you probably want to pick.
|
||||
3. [`v2`](https://github.com/9seconds/mtg/tree/v2) has a development
|
||||
of the v2.x version. In theory, it is the same as `master` but this
|
||||
will change when we have v3.x.
|
||||
4. [`v1`](https://github.com/9seconds/mtg/tree/v1) has a version 1.x.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Download a tool
|
||||
|
||||
#### Download binaries
|
||||
|
||||
Binaries can be downloaded from the release page. Also, you can download
|
||||
docker image.
|
||||
|
||||
For the current version, please download like
|
||||
|
||||
```console
|
||||
$ make
|
||||
docker pull nineseconds/mtg:2
|
||||
```
|
||||
|
||||
If you want to build for another platform:
|
||||
For version 1:
|
||||
|
||||
```console
|
||||
$ make crosscompile
|
||||
docker pull nineseconds/mtg:1
|
||||
```
|
||||
|
||||
If you want to build Docker image (called `mtg`):
|
||||
You may also check both [Docker
|
||||
Hub](https://hub.docker.com/r/nineseconds/mtg/tags) and [Github
|
||||
Registry](https://github.com/users/9seconds/packages/container/package/mtg).
|
||||
Please do not choose `latest` or `stable` if you want to avoid
|
||||
surprises. Always choose some version tag.
|
||||
|
||||
Also, if you have `go` installed, you can always download this tool with `go get`:
|
||||
|
||||
```console
|
||||
$ make docker
|
||||
go get github.com/9seconds/mtg/v2
|
||||
```
|
||||
|
||||
# Docker image
|
||||
|
||||
Docker follows the same policy as the source code organization:
|
||||
|
||||
- `latest` mirrors the master branch
|
||||
- `stable` mirrors the stable branch
|
||||
- tags are for tagged releases
|
||||
#### Build from sources
|
||||
|
||||
```console
|
||||
$ docker pull nineseconds/mtg:latest
|
||||
git clone https://github.com:9seconds/mtg.git
|
||||
cd mtg
|
||||
make static
|
||||
```
|
||||
|
||||
or for the docker image:
|
||||
|
||||
```console
|
||||
$ docker pull nineseconds/mtg:stable
|
||||
make docker
|
||||
```
|
||||
|
||||
```console
|
||||
$ docker pull nineseconds/mtg:0.10
|
||||
```
|
||||
### Generate secret
|
||||
|
||||
# Configuration
|
||||
|
||||
Basically, to run this tool you need to configure as less as possible.
|
||||
|
||||
First, you need to generate a secret:
|
||||
If you already have a secret in Base64 format or that, which starts with `ee`,
|
||||
you can skip this chapter. Otherwise:
|
||||
|
||||
```console
|
||||
$ openssl rand -hex 16
|
||||
$ mtg generate-secret google.com
|
||||
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```console
|
||||
$ head -c 512 /dev/urandom | md5sum | cut -f 1 -d ' '
|
||||
$ mtg generate-secret --hex google.com
|
||||
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||
```
|
||||
|
||||
## Secure mode
|
||||
This secret is a keystone for a proxy and your password for a client.
|
||||
You need to keep it secured.
|
||||
|
||||
If you want to support new secure mode, please prepend `dd` to the
|
||||
secret. For example, secret `cf18fa8ea0267057e2c61a5f7322a8e7` should
|
||||
be `ddcf18fa8ea0267057e2c61a5f7322a8e7`. But pay attention that some
|
||||
old clients won't support this mode. If this is not your case, I would
|
||||
suggest to go with this mode.
|
||||
We recommend choosing a hostname wisely. Here we have a _google.com_
|
||||
but in reality, all providers can easily detect that this is not a
|
||||
Google. Google has a list of networks it officially uses and your IP
|
||||
address won't probably belong to it. It is a great idea to hide behind
|
||||
some domain that has some relation to this IP address.
|
||||
|
||||
Oneliners to generate such secrets:
|
||||
For example, you've bought a VPS from [Digital
|
||||
Ocean](https://www.digitalocean.com/). Then it might be a good idea to
|
||||
generate a secret for _digitalocean.com_ then.
|
||||
|
||||
|
||||
### Simple run mode
|
||||
|
||||
mtg supports 2 modes: simple and normal. Simple mode allows starting
|
||||
proxy with a small subset of configuration options you usually want to
|
||||
modify. This is quite good for oneliners that you can copy-paste and do
|
||||
not bother about external files whatsoever.
|
||||
|
||||
Let's take a look:
|
||||
|
||||
```console
|
||||
$ echo dd$(openssl rand -hex 16)
|
||||
Usage: mtg simple-run <bind-to> <secret>
|
||||
|
||||
Run proxy without config file.
|
||||
|
||||
Arguments:
|
||||
<bind-to> A host:port to bind proxy to.
|
||||
<secret> Proxy secret.
|
||||
|
||||
Flags:
|
||||
-h, --help Show context-sensitive help.
|
||||
-v, --version Print version.
|
||||
|
||||
-d, --debug Run in debug mode.
|
||||
-c, --concurrency=8192 Max number of concurrent connection to proxy.
|
||||
-b, --tcp-buffer="4KB" Size of TCP buffer to use.
|
||||
-i, --prefer-ip="prefer-ipv6" IP preference. By default we prefer IPv6 with fallback to IPv4.
|
||||
-p, --domain-fronting-port=443 A port to access for domain fronting.
|
||||
-n, --doh-ip=9.9.9.9 IP address of DNS-over-HTTP to use.
|
||||
-t, --timeout=10s Network timeout to use
|
||||
-a, --antireplay-cache-size="1MB" A size of anti-replay cache to use.
|
||||
```
|
||||
|
||||
or
|
||||
So, if you want to startup a proxy with CLI only, you can do something like
|
||||
|
||||
```console
|
||||
$ echo dd$(head -c 512 /dev/urandom | md5sum | cut -f 1 -d ' ')
|
||||
$ mtg simple-run -n 1.1.1.1 -t 30s -a 512kib 127.0.0.1:3128 7hBO-dCS4EBzenlKbdLFxyNnb29nbGUuY29t
|
||||
```
|
||||
|
||||
The rest of the configuration will be taken from default values. But
|
||||
a simple run is fine if you do not have any special requirements or
|
||||
granular tuning. If you want it, please checkout the configuration
|
||||
files.
|
||||
|
||||
# How to run the tool
|
||||
### Prepare a configuration file
|
||||
|
||||
Now run the tool:
|
||||
Please checkout an example configuration file. All options except of
|
||||
`secret` and `bind-to` are optional. You can safely have this minimal
|
||||
configuration file:
|
||||
|
||||
```toml
|
||||
secret = "ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d"
|
||||
bind-to = "0.0.0.0:443"
|
||||
```
|
||||
|
||||
This is enough to run the whole application. All other
|
||||
options already have sensible defaults for the app at almost any scale.
|
||||
|
||||
Oh, the configuration is done in [TOML format](https://toml.io/en/).
|
||||
|
||||
### Run a proxy
|
||||
|
||||
Put a binary and a config into your webserver. Just for example,
|
||||
a binary goes to `/usr/local/bin/mtg` and configuration to `/etc/mtg.toml`.
|
||||
|
||||
Now you can create a systemd unit:
|
||||
|
||||
```console
|
||||
$ mtg <secret>
|
||||
$ cat /etc/systemd/system/mtg.service
|
||||
[Unit]
|
||||
Description=mtg
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
$ sudo systemctl daemon-reload
|
||||
$ sudo systemctl enable mtg
|
||||
$ sudo systemctl start mtg
|
||||
```
|
||||
|
||||
How to run the tool with ADTag:
|
||||
or you can run a docker image
|
||||
|
||||
```console
|
||||
$ mtg <secret> <adtag>
|
||||
docker run -d -v /etc/mtg.toml:/config.toml -p 443:3128 --restart=unless-stopped nineseconds/mtg:2
|
||||
```
|
||||
|
||||
This tool will listen on port 3128 by default with the given secret.
|
||||
where _443_ is a host port (a port you want to connect to from a
|
||||
client), and _3128_ is the one you have in your config in the `bind-to`
|
||||
section.
|
||||
|
||||
# One-line runner
|
||||
### Access a proxy
|
||||
|
||||
Now you can generate some useful links:
|
||||
|
||||
```console
|
||||
$ docker run --name mtg --restart=unless-stopped -p 3128:3128 -p 3129:3129 -d nineseconds/mtg:stable $(openssl rand -hex 16)
|
||||
$ mtg access /etc/mtg.toml
|
||||
{
|
||||
"ipv4": {
|
||||
"ip": "x.y.z.a",
|
||||
"port": 3128,
|
||||
"tg_url": "tg://proxy?...",
|
||||
"tg_qrcode": "https://api.qrserver.com/v1/create-qr-code?data...",
|
||||
"tme_url": "https://t.me/proxy?...",
|
||||
"tme_qrcode": "https://api.qrserver.com/v1/create-qr-code?data..."
|
||||
},
|
||||
"secret": {
|
||||
"hex": "...",
|
||||
"base64": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or in secret mode:
|
||||
## Metrics
|
||||
|
||||
```console
|
||||
$ docker run --name mtg --restart=unless-stopped -p 3128:3128 -p 3129:3129 -d nineseconds/mtg:stable dd$(openssl rand -hex 16)
|
||||
```
|
||||
Out of the box, mtg works with
|
||||
[statsd](https://github.com/statsd/statsd) and
|
||||
[Prometheus](https://prometheus.io/). Please check configuration file
|
||||
example to get how to set this integration up.
|
||||
|
||||
You will have this tool up and running on port 3128. Now curl
|
||||
`localhost:3129` to get `tg://` links or do `docker logs mtg`. Also,
|
||||
port 3129 will show you some statistics if you are interested in.
|
||||
Here goes a list of metrics with their types but without a prefix.
|
||||
|
||||
Also, you can use [run-mtg.sh](https://github.com/9seconds/mtg/blob/master/run-mtg.sh) script
|
||||
| Name | Type | Tags | Description |
|
||||
|-----------------------------|---------|----------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| client_connections | gauge | `ip_family` | Count of processing client connections. |
|
||||
| telegram_connections | gauge | `telegram_ip`, `dc` | Count of connections to Telegram servers. |
|
||||
| domain_fronting_connections | gauge | `ip_family` | Count of connections to fronting domain. |
|
||||
| telegram_traffic | counter | `telegram_ip`, `dc`, `direction` | Count of bytes, transmitted to/from Telegram. |
|
||||
| domain_fronting_traffic | counter | `direction` | Count of bytes, transmitted to/from fronting domain. |
|
||||
| domain_fronting | counter | – | Count of domain fronting events. |
|
||||
| concurrency_limited | counter | – | Count of events, when client connection was rejected due to concurrency limit. |
|
||||
| ip_blocklisted | counter | – | Count of events when client connection was rejected because IP was found in the blacklist. |
|
||||
| replay_attacks | counter | – | Count of detected replay attacks. |
|
||||
|
||||
Tag meaning:
|
||||
|
||||
# statsd integration
|
||||
|
||||
mtg provides an integration with statsd, you can enable it with command
|
||||
line interface. To enable it, you have to provide IP address of statsd
|
||||
service.
|
||||
|
||||
Out of the box, mtg supports 2 additional dialects: [InfluxDB](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/)
|
||||
and [Datadog](https://docs.datadoghq.com/developers/dogstatsd/).
|
||||
|
||||
All metrics are gauges. Here is the list of metrics and their meaning:
|
||||
|
||||
| Metric name | Unit | Description |
|
||||
|---------------------------------|---------|-----------------------------------------------------------|
|
||||
| `connections.abridged.ipv4` | number | The number of active abridged IPv4 connections |
|
||||
| `connections.abridged.ipv6` | number | The number of active abridged IPv6 connections |
|
||||
| `connections.intermediate.ipv4` | number | The number of active intermediate IPv4 connections |
|
||||
| `connections.intermediate.ipv6` | number | The number of active intermediate IPv6 connections |
|
||||
| `connections.secure.ipv4` | number | The number of active secure intermediate IPv4 connections |
|
||||
| `connections.secure.ipv6` | number | The number of active secure intermediate IPv6 connections |
|
||||
| `crashes` | number | An amount of crashes in client handlers |
|
||||
| `traffic.ingress` | bytes | Ingress traffic from the start of application (incoming) |
|
||||
| `traffic.egress` | bytes | Egress traffic from the start of application (outgoing) |
|
||||
| `speed.ingress` | bytes/s | Ingress bandwidth of the latest second (incoming traffic) |
|
||||
| `speed.egress` | bytes/s | Egress bandwidth of the latest second (outgoing traffic) |
|
||||
|
||||
All metrics are prefixed with given prefix. Default prefix is `mtg`.
|
||||
With such prefix metric name `traffic.ingress`, for example, would be
|
||||
`mtg.traffic.ingress`.
|
||||
| Name | Values | Description |
|
||||
|-------------|----------------------------|-----------------------------------------------|
|
||||
| ip_family | `ipv4`, `ipv6` | A version of the IP protocol. |
|
||||
| dc | | A number of the Telegram DC for a connection. |
|
||||
| telegram_ip | | IP address of the Telegram server. |
|
||||
| direction | `to_client`, `from_client` | A direction of the traffic flow. |
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We support 2 tracks: 2.x and 1.x
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x | :white_check_mark: |
|
||||
| 1.x | :interrobang: |
|
||||
| < 1.0 | :x: |
|
||||
|
||||
1.x has adtag support. We do not plan any active development there but we guarantee:
|
||||
|
||||
1. Regular dependency updates
|
||||
2. Updates for Golang versions
|
||||
3. Security vulnerability fixes
|
||||
4. Merging small PRs
|
||||
|
||||
2.x is in active development and have a full support.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you have found a vulnerability, please report about it to nineseconds@yandex.ru, telegram @9seconds or here in issues
|
||||
@@ -0,0 +1,17 @@
|
||||
// Antireplay package has cache implementations that are effective
|
||||
// against replay attacks.
|
||||
//
|
||||
// To understand more about replay attacks, please read documentation
|
||||
// for mtglib.AntiReplayCache interface. This package has a list of some
|
||||
// implementations of this interface.
|
||||
package antireplay
|
||||
|
||||
const (
|
||||
// DefaultStableBloomFilterMaxSize is a recommended byte size for a
|
||||
// stable bloom filter.
|
||||
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
|
||||
|
||||
// DefaultStableBloomFilterErrorRate is a recommended default error
|
||||
// rate for a stable bloom filter.
|
||||
DefaultStableBloomFilterErrorRate = 0.001
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package antireplay
|
||||
|
||||
import "github.com/9seconds/mtg/v2/mtglib"
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) SeenBefore(_ []byte) bool { return false }
|
||||
|
||||
// NewNoop returns an implementation that does nothing. A corresponding
|
||||
// method always returns false, so this cache accepts everything you
|
||||
// pass to it.
|
||||
func NewNoop() mtglib.AntiReplayCache {
|
||||
return noop{}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package antireplay_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestOp() {
|
||||
filter := antireplay.NewNoop()
|
||||
|
||||
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package antireplay
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/OneOfOne/xxhash"
|
||||
boom "github.com/tylertreat/BoomFilters"
|
||||
)
|
||||
|
||||
type stableBloomFilter struct {
|
||||
filter boom.StableBloomFilter
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (s *stableBloomFilter) SeenBefore(digest []byte) bool {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
return s.filter.TestAndAdd(digest)
|
||||
}
|
||||
|
||||
// NewStableBloomFilter returns an implementation of AntiReplayCache
|
||||
// based on stable bloom filter.
|
||||
//
|
||||
// http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf
|
||||
//
|
||||
// The basic idea of a stable bloom filter is quite simple: each time
|
||||
// when you set a new element, you randomly reset P elements. There is a
|
||||
// hardcore math which proves that if you choose this P correctly, you
|
||||
// can maintain the same error rate for a stream of elements.
|
||||
//
|
||||
// byteSize is the number of bytes you want to give to a bloom filter.
|
||||
// errorRate is desired false-positive error rate. If you want to use
|
||||
// default values, please pass 0 for byteSize and <0 for errorRate.
|
||||
func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache {
|
||||
if byteSize == 0 {
|
||||
byteSize = DefaultStableBloomFilterMaxSize
|
||||
}
|
||||
|
||||
if errorRate < 0 {
|
||||
errorRate = DefaultStableBloomFilterErrorRate
|
||||
}
|
||||
|
||||
sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate) // nolint: gomnd
|
||||
sf.SetHash(xxhash.New64())
|
||||
|
||||
return &stableBloomFilter{
|
||||
filter: *sf,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package antireplay_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type StableBloomFilterTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *StableBloomFilterTestSuite) TestOp() {
|
||||
filter := antireplay.NewStableBloomFilter(500, 0.001)
|
||||
|
||||
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
suite.True(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.True(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
}
|
||||
|
||||
func TestStableBloomFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &StableBloomFilterTestSuite{})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/wrappers"
|
||||
)
|
||||
|
||||
// Init defines common method for initializing client connections.
|
||||
type Init func(net.Conn, string, *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error)
|
||||
@@ -1,55 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/juju/errors"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/obfuscated2"
|
||||
"github.com/9seconds/mtg/wrappers"
|
||||
)
|
||||
|
||||
const (
|
||||
handshakeTimeout = 10 * time.Second
|
||||
readBufferSize = 64 * 1024
|
||||
writeBufferSize = 64 * 1024
|
||||
)
|
||||
|
||||
// DirectInit initializes client connection for proxy which connects to
|
||||
// Telegram directly.
|
||||
func DirectInit(socket net.Conn, connID string, conf *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) {
|
||||
tcpSocket := socket.(*net.TCPConn)
|
||||
if err := tcpSocket.SetNoDelay(false); err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot disable NO_DELAY to client socket")
|
||||
}
|
||||
if err := tcpSocket.SetReadBuffer(readBufferSize); err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot set read buffer size of client socket")
|
||||
}
|
||||
if err := tcpSocket.SetWriteBuffer(writeBufferSize); err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot set write buffer size of client socket")
|
||||
}
|
||||
|
||||
socket.SetReadDeadline(time.Now().Add(handshakeTimeout)) // nolint: errcheck
|
||||
frame, err := obfuscated2.ExtractFrame(socket)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot extract frame")
|
||||
}
|
||||
socket.SetReadDeadline(time.Time{}) // nolint: errcheck
|
||||
|
||||
conn := wrappers.NewConn(socket, connID, wrappers.ConnPurposeClient, conf.PublicIPv4, conf.PublicIPv6)
|
||||
obfs2, connOpts, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot parse obfuscated frame")
|
||||
}
|
||||
connOpts.ConnectionProto = mtproto.ConnectionProtocolAny
|
||||
connOpts.ClientAddr = conn.RemoteAddr()
|
||||
|
||||
conn = wrappers.NewStreamCipher(conn, obfs2.Encryptor, obfs2.Decryptor)
|
||||
|
||||
conn.Logger().Infow("Client connection initialized")
|
||||
|
||||
return conn, connOpts, nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/wrappers"
|
||||
)
|
||||
|
||||
// MiddleInit initializes client connection for proxy which has to
|
||||
// support promoted channels, connect to Telegram middle proxies etc.
|
||||
func MiddleInit(socket net.Conn, connID string, conf *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) {
|
||||
conn, opts, err := DirectInit(socket, connID, conf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
connStream := conn.(wrappers.StreamReadWriteCloser)
|
||||
|
||||
var newConn wrappers.PacketReadWriteCloser
|
||||
switch opts.ConnectionType {
|
||||
case mtproto.ConnectionTypeAbridged:
|
||||
newConn = wrappers.NewMTProtoAbridged(connStream, opts)
|
||||
case mtproto.ConnectionTypeIntermediate:
|
||||
newConn = wrappers.NewMTProtoIntermediate(connStream, opts)
|
||||
case mtproto.ConnectionTypeSecure:
|
||||
newConn = wrappers.NewMTProtoIntermediateSecure(connStream, opts)
|
||||
default:
|
||||
panic("Unknown connection type")
|
||||
}
|
||||
|
||||
opts.ConnectionProto = mtproto.ConnectionProtocolIPv4
|
||||
if socket.LocalAddr().(*net.TCPAddr).IP.To4() == nil {
|
||||
opts.ConnectionProto = mtproto.ConnectionProtocolIPv6
|
||||
}
|
||||
|
||||
return newConn, opts, err
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/juju/errors"
|
||||
statsd "gopkg.in/alexcesaro/statsd.v2"
|
||||
)
|
||||
|
||||
// Buffer sizes define internal socket buffer sizes.
|
||||
const (
|
||||
BufferWriteSize = 32 * 1024
|
||||
BufferReadSize = 32 * 1024
|
||||
)
|
||||
|
||||
// Config represents common configuration of mtg.
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Verbose bool
|
||||
SecureMode bool
|
||||
|
||||
BindPort uint16
|
||||
PublicIPv4Port uint16
|
||||
PublicIPv6Port uint16
|
||||
StatsPort uint16
|
||||
|
||||
BindIP net.IP
|
||||
PublicIPv4 net.IP
|
||||
PublicIPv6 net.IP
|
||||
StatsIP net.IP
|
||||
|
||||
StatsD struct {
|
||||
Addr net.Addr
|
||||
Prefix string
|
||||
Tags map[string]string
|
||||
TagsFormat statsd.TagFormat
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
Secret []byte
|
||||
AdTag []byte
|
||||
}
|
||||
|
||||
// URLs contains links to the proxy (tg://, t.me) and their QR codes.
|
||||
type URLs struct {
|
||||
TG string `json:"tg_url"`
|
||||
TMe string `json:"tme_url"`
|
||||
TGQRCode string `json:"tg_qrcode"`
|
||||
TMeQRCode string `json:"tme_qrcode"`
|
||||
}
|
||||
|
||||
// IPURLs contains links to both ipv4 and ipv6 of the proxy.
|
||||
type IPURLs struct {
|
||||
IPv4 URLs `json:"ipv4"`
|
||||
IPv6 URLs `json:"ipv6"`
|
||||
BotSecret string `json:"secret_for_mtproxybot"`
|
||||
}
|
||||
|
||||
// BindAddr returns connection for this server to bind to.
|
||||
func (c *Config) BindAddr() string {
|
||||
return getAddr(c.BindIP, c.BindPort)
|
||||
}
|
||||
|
||||
// StatAddr returns connection string to the stats API.
|
||||
func (c *Config) StatAddr() string {
|
||||
return getAddr(c.StatsIP, c.StatsPort)
|
||||
}
|
||||
|
||||
// UseMiddleProxy defines if this proxy has to connect middle proxies
|
||||
// which supports promoted channels or directly access Telegram.
|
||||
func (c *Config) UseMiddleProxy() bool {
|
||||
return len(c.AdTag) > 0
|
||||
}
|
||||
|
||||
// BotSecretString returns secret string which should work with MTProxybot.
|
||||
func (c *Config) BotSecretString() string {
|
||||
return hex.EncodeToString(c.Secret)
|
||||
}
|
||||
|
||||
// SecretString returns a secret in a form entered on the start of the
|
||||
// application.
|
||||
func (c *Config) SecretString() string {
|
||||
secret := c.BotSecretString()
|
||||
if c.SecureMode {
|
||||
return "dd" + secret
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
// GetURLs returns configured IPURLs instance with links to this server.
|
||||
func (c *Config) GetURLs() IPURLs {
|
||||
urls := IPURLs{}
|
||||
secret := c.SecretString()
|
||||
if c.PublicIPv4 != nil {
|
||||
urls.IPv4 = getURLs(c.PublicIPv4, c.PublicIPv4Port, secret)
|
||||
}
|
||||
if c.PublicIPv6 != nil {
|
||||
urls.IPv6 = getURLs(c.PublicIPv6, c.PublicIPv6Port, secret)
|
||||
}
|
||||
urls.BotSecret = c.BotSecretString()
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
func getAddr(host fmt.Stringer, port uint16) string {
|
||||
return net.JoinHostPort(host.String(), strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
// NewConfig returns new configuration. If required, it manages and
|
||||
// fetches data from external sources. Parameters passed to this
|
||||
// function, should come from command line arguments.
|
||||
func NewConfig(debug, verbose bool, // nolint: gocyclo
|
||||
bindIP net.IP, bindPort uint16,
|
||||
publicIPv4 net.IP, PublicIPv4Port uint16,
|
||||
publicIPv6 net.IP, publicIPv6Port uint16,
|
||||
statsIP net.IP, statsPort uint16,
|
||||
secret, adtag string,
|
||||
statsdIP string, statsdPort uint16, statsdNetwork string, statsdPrefix string,
|
||||
statsdTagsFormat string, statsdTags map[string]string) (*Config, error) {
|
||||
secureMode := false
|
||||
if strings.HasPrefix(secret, "dd") && len(secret) == 34 {
|
||||
secureMode = true
|
||||
secret = strings.TrimPrefix(secret, "dd")
|
||||
} else if len(secret) != 32 {
|
||||
return nil, errors.New("Telegram demands secret of length 32")
|
||||
}
|
||||
secretBytes, err := hex.DecodeString(secret)
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "Cannot create config")
|
||||
}
|
||||
|
||||
var adTagBytes []byte
|
||||
if len(adtag) != 0 {
|
||||
adTagBytes, err = hex.DecodeString(adtag)
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "Cannot create config")
|
||||
}
|
||||
}
|
||||
|
||||
if publicIPv4 == nil {
|
||||
publicIPv4, err = getGlobalIPv4()
|
||||
if err != nil {
|
||||
publicIPv4 = nil
|
||||
} else if publicIPv4.To4() == nil {
|
||||
return nil, errors.Errorf("IP %s is not IPv4", publicIPv4.String())
|
||||
}
|
||||
}
|
||||
if PublicIPv4Port == 0 {
|
||||
PublicIPv4Port = bindPort
|
||||
}
|
||||
|
||||
if publicIPv6 == nil {
|
||||
publicIPv6, err = getGlobalIPv6()
|
||||
if err != nil {
|
||||
publicIPv6 = nil
|
||||
} else if publicIPv6.To4() != nil {
|
||||
return nil, errors.Errorf("IP %s is not IPv6", publicIPv6.String())
|
||||
}
|
||||
}
|
||||
if publicIPv6Port == 0 {
|
||||
publicIPv6Port = bindPort
|
||||
}
|
||||
|
||||
if statsIP == nil {
|
||||
statsIP = publicIPv4
|
||||
}
|
||||
|
||||
conf := &Config{
|
||||
Debug: debug,
|
||||
Verbose: verbose,
|
||||
BindIP: bindIP,
|
||||
BindPort: bindPort,
|
||||
PublicIPv4: publicIPv4,
|
||||
PublicIPv4Port: PublicIPv4Port,
|
||||
PublicIPv6: publicIPv6,
|
||||
PublicIPv6Port: publicIPv6Port,
|
||||
StatsIP: statsIP,
|
||||
StatsPort: statsPort,
|
||||
Secret: secretBytes,
|
||||
AdTag: adTagBytes,
|
||||
SecureMode: secureMode,
|
||||
}
|
||||
|
||||
if statsdIP != "" {
|
||||
conf.StatsD.Enabled = true
|
||||
conf.StatsD.Prefix = statsdPrefix
|
||||
conf.StatsD.Tags = statsdTags
|
||||
|
||||
var addr net.Addr
|
||||
hostPort := net.JoinHostPort(statsdIP, strconv.Itoa(int(statsdPort)))
|
||||
switch statsdNetwork {
|
||||
case "tcp":
|
||||
addr, err = net.ResolveTCPAddr("tcp", hostPort)
|
||||
case "udp":
|
||||
addr, err = net.ResolveUDPAddr("udp", hostPort)
|
||||
default:
|
||||
err = errors.Errorf("Unknown network %s", statsdNetwork)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "Cannot resolve statsd address")
|
||||
}
|
||||
conf.StatsD.Addr = addr
|
||||
|
||||
switch statsdTagsFormat {
|
||||
case "datadog":
|
||||
conf.StatsD.TagsFormat = statsd.Datadog
|
||||
case "influxdb":
|
||||
conf.StatsD.TagsFormat = statsd.InfluxDB
|
||||
case "":
|
||||
default:
|
||||
return nil, errors.Errorf("Unknown tags format %s", statsdTagsFormat)
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/juju/errors"
|
||||
)
|
||||
|
||||
func getGlobalIPv4() (net.IP, error) {
|
||||
return fetchIP("https://v4.ifconfig.co/ip")
|
||||
}
|
||||
|
||||
func getGlobalIPv6() (net.IP, error) {
|
||||
return fetchIP("https://v6.ifconfig.co/ip")
|
||||
}
|
||||
|
||||
func fetchIP(url string) (net.IP, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
|
||||
respDataBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respData := strings.TrimSpace(string(respDataBytes))
|
||||
|
||||
ip := net.ParseIP(respData)
|
||||
if ip == nil {
|
||||
return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", respData)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func getURLs(addr net.IP, port uint16, secret string) (urls URLs) {
|
||||
values := url.Values{}
|
||||
values.Set("server", addr.String())
|
||||
values.Set("port", strconv.Itoa(int(port)))
|
||||
values.Set("secret", secret)
|
||||
|
||||
urls.TG = makeTGURL(values)
|
||||
urls.TMe = makeTMeURL(values)
|
||||
urls.TGQRCode = makeQRCodeURL(urls.TG)
|
||||
urls.TMeQRCode = makeQRCodeURL(urls.TG)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeTGURL(values url.Values) string {
|
||||
tgURL := url.URL{
|
||||
Scheme: "tg",
|
||||
Host: "proxy",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return tgURL.String()
|
||||
}
|
||||
|
||||
func makeTMeURL(values url.Values) string {
|
||||
tMeURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "t.me",
|
||||
Path: "proxy",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return tMeURL.String()
|
||||
}
|
||||
|
||||
func makeQRCodeURL(data string) string {
|
||||
QRURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.qrserver.com",
|
||||
Path: "v1/create-qr-code",
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("qzone", "4")
|
||||
values.Set("format", "svg")
|
||||
values.Set("data", data)
|
||||
QRURL.RawQuery = values.Encode()
|
||||
|
||||
return QRURL.String()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/OneOfOne/xxhash"
|
||||
)
|
||||
|
||||
// EventStream is a default implementation of the mtglib.EventStream
|
||||
// interface.
|
||||
//
|
||||
// EventStream manages a set of goroutines, observers. Main
|
||||
// responsibility of the event stream is to route an event to relevant
|
||||
// observer based on some hash so each observer will have all events
|
||||
// which belong to some stream id.
|
||||
//
|
||||
// Thus, EventStream can spawn many observers.
|
||||
type EventStream struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
chans []chan mtglib.Event
|
||||
}
|
||||
|
||||
// Send starts delivering of the message to observer with respect to a
|
||||
// given context If context is closed, message could be not delivered.
|
||||
func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
|
||||
var chanNo uint32
|
||||
|
||||
if streamID := evt.StreamID(); streamID != "" {
|
||||
chanNo = xxhash.ChecksumString32(streamID)
|
||||
} else {
|
||||
chanNo = rand.Uint32()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-e.ctx.Done():
|
||||
case e.chans[int(chanNo)%len(e.chans)] <- evt:
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops an event stream pipeline.
|
||||
func (e EventStream) Shutdown() {
|
||||
e.ctxCancel()
|
||||
}
|
||||
|
||||
// NewEventStream builds a new default event stream.
|
||||
//
|
||||
// If you give an empty array of observers, then NoopObserver is going
|
||||
// to be used. If you give many observers, then they will process a
|
||||
// message concurrently.
|
||||
func NewEventStream(observerFactories []ObserverFactory) EventStream {
|
||||
if len(observerFactories) == 0 {
|
||||
observerFactories = append(observerFactories, NewNoopObserver)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
rv := EventStream{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
chans: make([]chan mtglib.Event, runtime.NumCPU()),
|
||||
}
|
||||
|
||||
for i := 0; i < runtime.NumCPU(); i++ {
|
||||
rv.chans[i] = make(chan mtglib.Event, 1)
|
||||
|
||||
if len(observerFactories) == 1 {
|
||||
go eventStreamProcessor(ctx, rv.chans[i], observerFactories[0]())
|
||||
} else {
|
||||
go eventStreamProcessor(ctx, rv.chans[i], newMultiObserver(observerFactories))
|
||||
}
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { // nolint: cyclop
|
||||
defer observer.Shutdown()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case evt := <-eventChan:
|
||||
switch typedEvt := evt.(type) {
|
||||
case mtglib.EventTraffic:
|
||||
observer.EventTraffic(typedEvt)
|
||||
case mtglib.EventStart:
|
||||
observer.EventStart(typedEvt)
|
||||
case mtglib.EventFinish:
|
||||
observer.EventFinish(typedEvt)
|
||||
case mtglib.EventConnectedToDC:
|
||||
observer.EventConnectedToDC(typedEvt)
|
||||
case mtglib.EventDomainFronting:
|
||||
observer.EventDomainFronting(typedEvt)
|
||||
case mtglib.EventIPBlocklisted:
|
||||
observer.EventIPBlocklisted(typedEvt)
|
||||
case mtglib.EventConcurrencyLimited:
|
||||
observer.EventConcurrencyLimited(typedEvt)
|
||||
case mtglib.EventReplayAttack:
|
||||
observer.EventReplayAttack(typedEvt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type EventStreamTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
observerMock1 *ObserverMock
|
||||
observerMock2 *ObserverMock
|
||||
stream events.EventStream
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) SetupTest() {
|
||||
suite.ctx, suite.ctxCancel = context.WithCancel(context.Background())
|
||||
|
||||
suite.observerMock1 = &ObserverMock{}
|
||||
suite.observerMock2 = &ObserverMock{}
|
||||
|
||||
suite.observerMock1.On("Shutdown")
|
||||
suite.observerMock2.On("Shutdown")
|
||||
|
||||
factories := make([]events.ObserverFactory, 2)
|
||||
factories[0] = func() events.Observer { return suite.observerMock1 }
|
||||
factories[1] = func() events.Observer { return suite.observerMock2 }
|
||||
|
||||
suite.stream = events.NewEventStream(factories)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventStart() {
|
||||
evt := mtglib.NewEventStart("connID", net.ParseIP("10.0.0.1"))
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventStart", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventStart)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventConnectedToDC() {
|
||||
evt := mtglib.NewEventConnectedToDC("connID", net.ParseIP("10.0.0.1"), 3)
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventConnectedToDC", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventConnectedToDC)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.DC, caught.DC)
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventDomainFronting() {
|
||||
evt := mtglib.NewEventDomainFronting("connID")
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventDomainFronting", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventDomainFronting)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventTraffic() {
|
||||
evt := mtglib.NewEventTraffic("connID", 1024, true)
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventTraffic", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventTraffic)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
suite.Equal(evt.Traffic, caught.Traffic)
|
||||
suite.Equal(evt.IsRead, caught.IsRead)
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventFinish() {
|
||||
evt := mtglib.NewEventFinish("connID")
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventFinish", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventFinish)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventConcurrencyLimited() {
|
||||
evt := mtglib.NewEventConcurrencyLimited()
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventConcurrencyLimited", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventConcurrencyLimited)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
suite.Empty(evt.StreamID())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventIPBlocklisted() {
|
||||
evt := mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10"))
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventIPBlocklisted", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventIPBlocklisted)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventReplayAttack() {
|
||||
evt := mtglib.NewEventReplayAttack("CONNID")
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventReplayAttack", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventReplayAttack)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TearDownTest() {
|
||||
suite.stream.Shutdown()
|
||||
suite.ctxCancel()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
suite.observerMock1.AssertExpectations(suite.T())
|
||||
suite.observerMock2.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func TestEventStream(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &EventStreamTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Events has a default implementations of EventStream for mtglib.
|
||||
//
|
||||
// Please see documentation for mtglib.EventStream interface to get an
|
||||
// idea of such an abstraction. This package has implementations for the
|
||||
// default event stream.
|
||||
//
|
||||
// Default event stream has a list of its own concepts. First, all it
|
||||
// does is a routing of messages to known observers. It takes an event,
|
||||
// defines its type and pass this message to a method of the observer.
|
||||
//
|
||||
// There might be many observers, but default event stream has a
|
||||
// guarantee though. It uses StreamID as a sharding key and guarantees
|
||||
// that a message with the same StreamID will be devlivered to the same
|
||||
// observer instance. So, each producer is guarateed to get all relevant
|
||||
// messages related to the same session. It is not possible that it will
|
||||
// get EventFinish if it has not seen EventStart for that session yet.
|
||||
package events
|
||||
|
||||
import "github.com/9seconds/mtg/v2/mtglib"
|
||||
|
||||
// Observer is an instance that listens for the incoming events.
|
||||
//
|
||||
// As it is said in the package description, the default event stream
|
||||
// guarantees that all events with the same StreamID are going to be
|
||||
// routed to the same instance of the observer. So, there is no need
|
||||
// to synchronize information about streams between many observers
|
||||
// instances, they can have their local storage.
|
||||
type Observer interface {
|
||||
// EventStart reacts on incoming mtglib.EventStart event.
|
||||
EventStart(mtglib.EventStart)
|
||||
|
||||
// EventFinish reacts on incoming mtglib.EventFinish event.
|
||||
EventFinish(mtglib.EventFinish)
|
||||
|
||||
// EventConnectedToDC reacts on incoming mtglib.EventConnectedToDC
|
||||
// event.
|
||||
EventConnectedToDC(mtglib.EventConnectedToDC)
|
||||
|
||||
// EventDomainFronting reacts on incoming mtglib.EventDomainFronting
|
||||
// event.
|
||||
EventDomainFronting(mtglib.EventDomainFronting)
|
||||
|
||||
// EventTraffic reacts on incoming mtglib.EventTraffic event.
|
||||
EventTraffic(mtglib.EventTraffic)
|
||||
|
||||
// EventConcurrencyLimited reacts on incoming
|
||||
// mtglib.EventConcurrencyLimited event.
|
||||
EventConcurrencyLimited(mtglib.EventConcurrencyLimited)
|
||||
|
||||
// EventIPBlocklisted reacts on incoming mtglib.EventIPBlocklisted event.
|
||||
EventIPBlocklisted(mtglib.EventIPBlocklisted)
|
||||
|
||||
// EventReplayAttack reacts on incoming mtglib.EventReplayAttack event.
|
||||
EventReplayAttack(mtglib.EventReplayAttack)
|
||||
|
||||
// Shutdown stop observer. Default event stream guarantees:
|
||||
// 1. If shutdown is executed, it is executed only once
|
||||
// 2. Observer won't receieve any new message after this
|
||||
// function call.
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// ObserverFactory creates a new instance of the observer.
|
||||
//
|
||||
// Default event stream creates a small set of goroutines to manage
|
||||
// incoming messages. Each message is routed to an appropriate observer
|
||||
// based on a sharding key, stream id. So, it is possible that an
|
||||
// instance of mtg will have many observer instances, not a single one.
|
||||
type ObserverFactory func() Observer
|
||||
@@ -0,0 +1,46 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type ObserverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventStart(evt mtglib.EventStart) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventDomainFronting(evt mtglib.EventDomainFronting) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventTraffic(evt mtglib.EventTraffic) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventFinish(evt mtglib.EventFinish) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) Shutdown() {
|
||||
o.Called()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type multiObserver struct {
|
||||
observers []Observer
|
||||
}
|
||||
|
||||
func (m multiObserver) EventStart(evt mtglib.EventStart) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventStart(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventConnectedToDC(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventDomainFronting(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventTraffic(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventFinish(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventConcurrencyLimited(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventIPBlocklisted(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventReplayAttack(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) Shutdown() {
|
||||
for _, v := range m.observers {
|
||||
v.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func newMultiObserver(factories []ObserverFactory) Observer {
|
||||
observers := make([]Observer, len(factories))
|
||||
|
||||
for i, v := range factories {
|
||||
observers[i] = v()
|
||||
}
|
||||
|
||||
return multiObserver{
|
||||
observers: observers,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) Send(ctx context.Context, evt mtglib.Event) {}
|
||||
|
||||
// NewNoopStream creates a stream which discards each message.
|
||||
func NewNoopStream() mtglib.EventStream {
|
||||
return noop{}
|
||||
}
|
||||
|
||||
type noopObserver struct{}
|
||||
|
||||
func (n noopObserver) EventStart(_ mtglib.EventStart) {}
|
||||
func (n noopObserver) EventConnectedToDC(_ mtglib.EventConnectedToDC) {}
|
||||
func (n noopObserver) EventDomainFronting(_ mtglib.EventDomainFronting) {}
|
||||
func (n noopObserver) EventTraffic(_ mtglib.EventTraffic) {}
|
||||
func (n noopObserver) EventFinish(_ mtglib.EventFinish) {}
|
||||
func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {}
|
||||
func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) {}
|
||||
func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack) {}
|
||||
func (n noopObserver) Shutdown() {}
|
||||
|
||||
// NewNoopObserver creates an observer which discards each message.
|
||||
func NewNoopObserver() Observer {
|
||||
return noopObserver{}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
testData map[string]mtglib.Event
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) SetupSuite() {
|
||||
suite.testData = map[string]mtglib.Event{
|
||||
"start": mtglib.NewEventStart("connID", net.ParseIP("127.0.0.1")),
|
||||
"connected-to-dc": mtglib.NewEventConnectedToDC("connID", net.ParseIP("127.1.0.1"), 2),
|
||||
"domain-fronting": mtglib.NewEventDomainFronting("connID"),
|
||||
"traffic": mtglib.NewEventTraffic("connID", 1000, true),
|
||||
"finish": mtglib.NewEventFinish("connID"),
|
||||
"concurrency-limited": mtglib.NewEventConcurrencyLimited(),
|
||||
"ip-blacklisted": mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")),
|
||||
"replay-attack": mtglib.NewEventReplayAttack("connID"),
|
||||
}
|
||||
suite.ctx = context.Background()
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestStream() {
|
||||
stream := events.NewNoopStream()
|
||||
|
||||
for name, v := range suite.testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
stream.Send(suite.ctx, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestObserver() {
|
||||
observer := events.NewNoopObserver()
|
||||
|
||||
for name, v := range suite.testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
switch typedEvt := value.(type) {
|
||||
case mtglib.EventStart:
|
||||
observer.EventStart(typedEvt)
|
||||
case mtglib.EventConnectedToDC:
|
||||
observer.EventConnectedToDC(typedEvt)
|
||||
case mtglib.EventDomainFronting:
|
||||
observer.EventDomainFronting(typedEvt)
|
||||
case mtglib.EventFinish:
|
||||
observer.EventFinish(typedEvt)
|
||||
case mtglib.EventConcurrencyLimited:
|
||||
observer.EventConcurrencyLimited(typedEvt)
|
||||
case mtglib.EventIPBlocklisted:
|
||||
observer.EventIPBlocklisted(typedEvt)
|
||||
case mtglib.EventReplayAttack:
|
||||
observer.EventReplayAttack(typedEvt)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
observer.Shutdown()
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
# This is an example of the configuration file for mtg. You actually can
|
||||
# run mtg with it. It starts a proxy on all interfaces with a secret
|
||||
# ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d
|
||||
#
|
||||
# It has all possible options with default values. So, a real world
|
||||
# configuration file should contain only those options you are going to
|
||||
# use. You do not need to enumerate all of them. In other words, each
|
||||
# option here has a default value. If you comment a key-value pair, it
|
||||
# should not make any effect.
|
||||
#
|
||||
# stats is the only exception.
|
||||
|
||||
# Debug starts application in debug mode. It starts to be quite verbose
|
||||
# in output. Actually, the idea is that you run it in debug mode only if
|
||||
# you have any issue.
|
||||
debug = true
|
||||
|
||||
# A secret. Please remember that mtg supports only FakeTLS mode, legacy
|
||||
# simple and secured mode are prohibited. For you it means that secret
|
||||
# should either be base64-encoded or starts with ee.
|
||||
secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d"
|
||||
|
||||
# Host:port pair to run proxy on.
|
||||
bind-to = "0.0.0.0:3128"
|
||||
|
||||
# Defines how many concurrent connections are allowed to this proxy.
|
||||
# All other incoming connections are going to be dropped.
|
||||
concurrency = 8192
|
||||
|
||||
# A size of user-space buffer for TCP to use. Since we do 2 connections,
|
||||
# then we have tcp-buffer * (4 + 2) per each connection: read/write for
|
||||
# each connection + 2 copy buffers to pump the data between sockets.
|
||||
tcp-buffer = "4kb"
|
||||
|
||||
# Sometimes you want to enforce mtg to use some types of
|
||||
# IP connectivity to Telegram. We have 4 modes:
|
||||
# - prefer-ipv6:
|
||||
# We can use both ipv4 and ipv6 but ipv6 has a preference
|
||||
# - prefer-ipv4:
|
||||
# We can use both ipv4 and ipv6 but ipv4 has a preference
|
||||
# - only-ipv6:
|
||||
# Only ipv6 connectivity is used
|
||||
# - only-ipv4:
|
||||
# Only ipv4 connectivity is used
|
||||
prefer-ip = "prefer-ipv6"
|
||||
|
||||
# FakeTLS uses domain fronting protection. So it needs to know a port to
|
||||
# access.
|
||||
domain-fronting-port = 443
|
||||
|
||||
# FakeTLS can compare timestamps to prevent probes. Each message has
|
||||
# encrypted timestamp. So, mtg can compare this timestamp and decide if
|
||||
# we need to proceed with connection or not.
|
||||
#
|
||||
# Sometimes time can be skewed so we accept all messages within a
|
||||
# time range of this parameter.
|
||||
tolerate-time-skewness = "5s"
|
||||
|
||||
# Telegram has a concept of DC. You can think about DC as a number of a cluster
|
||||
# with a certain purpose. Some clusters serve media, some - messages, some rule
|
||||
# channels and so on. But sometimes unknown DC number is requested by client.
|
||||
# It could be a bug or some global reconfiguration of the Telegram.
|
||||
#
|
||||
# By default, proxy rejects such requests. But it is also possible to fallback
|
||||
# this request to any DC. Telegram works in a way that any DC is able to serve
|
||||
# any request but sacrificing a latency.
|
||||
#
|
||||
# If this setting is disabled (default), mtg will reject a connection.
|
||||
# Otherwise, chose a new DC.
|
||||
allow-fallback-on-unknown-dc = false
|
||||
|
||||
# network defines different network-related settings
|
||||
[network]
|
||||
# please be aware that mtg needs to do some external requests. For
|
||||
# example, if you do not pass public ips, it will request your public ip
|
||||
# address from some external service.
|
||||
#
|
||||
# As for 2.0, if you set a public-ip on your own, mtg won't issue any
|
||||
# network requests except of those required for Telegram.
|
||||
#
|
||||
# so, in order of doing them, it needs to do DNS lookup. mtg ignores DNS
|
||||
# resolver of the operating system and uses DOH instead. This is a host
|
||||
# it has to access.
|
||||
#
|
||||
# By default we use Quad9.
|
||||
doh-ip = "9.9.9.9"
|
||||
|
||||
# mtg can work via proxies (for now, we support only socks5). Proxy
|
||||
# configuration is done via list. So, you can specify many proxies
|
||||
# there.
|
||||
#
|
||||
# Actually, if you supply an empty list, then no proxies are going to be
|
||||
# used. If you supply a single proxy, then mtg will use it exclusively.
|
||||
# If you supply >= 2, then mtg will load balance between them.
|
||||
#
|
||||
# If you add an empty string here, this is an equivalent of 'plain network',
|
||||
# with no proxy usage.
|
||||
#
|
||||
# Proxy configuration is done via ordinary URI schema:
|
||||
#
|
||||
# socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s
|
||||
#
|
||||
# Only socks5 proxy is used. user/password is optional. As you can
|
||||
# see, you can specify some parameters in GET query. These parameters
|
||||
# configure circuit breaker.
|
||||
#
|
||||
# open_threshold means a number of errors which should happen so we stop
|
||||
# use a proxy.
|
||||
#
|
||||
# half_open_timeout means a time period (in Golang duration notation)
|
||||
# after which we can retry with this proxy
|
||||
#
|
||||
# reset_failures_timeout means a time period when we flush out errors
|
||||
# when circuit breaker in closed state.
|
||||
#
|
||||
# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
|
||||
# on details about circuit breakers.
|
||||
proxies = [
|
||||
# "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s"
|
||||
]
|
||||
|
||||
# network timeouts define different settings for timeouts. tcp timeout
|
||||
# define a global timeout on establishing of network connections. idle
|
||||
# means a timeout on pumping data between sockset when nothing is
|
||||
# happening.
|
||||
#
|
||||
# please be noticed that handshakes have no timeouts intentionally. You can
|
||||
# find a reasoning here:
|
||||
# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf
|
||||
[network.timeout]
|
||||
tcp = "5s"
|
||||
http = "10s"
|
||||
idle = "1m"
|
||||
|
||||
# Some countries do active probing on Telegram connections. This technique
|
||||
# allows to protect from such effort.
|
||||
#
|
||||
# mtg has a cache of some connection fingerprints. Actually, first bytes
|
||||
# of each connection. So, it stores them in some in-memory LRU+TTL cache.
|
||||
# You can configure this cache here.
|
||||
[defense.anti-replay]
|
||||
# You can enable/disable this feature.
|
||||
enabled = true
|
||||
# max size of such a cache. Please be aware that this number is
|
||||
# approximate we try hard to store data quite dense but it is possible
|
||||
# that we can go over this limit for 10-20% under some conditions and
|
||||
# architectures.
|
||||
max-size = "1mib"
|
||||
# we use stable bloom filters for anti-replay cache. This helps
|
||||
# to maintain a desired error ratio.
|
||||
error-rate = 0.001
|
||||
|
||||
# You can protect proxies by using different blocklists. If client has
|
||||
# ip from the given range, we do not try to do a proper handshake. We
|
||||
# actually route it to fronting domain. So, this client will never ever
|
||||
# have a chance to use mtg to access Telegram.
|
||||
#
|
||||
# Please remember that blocklists are initialized in async way. So,
|
||||
# when you start a proxy, blocklists are empty, they are populated and
|
||||
# processed in backgrounds. An error in any URL is ignored.
|
||||
[defense.blocklist]
|
||||
# You can enable/disable this feature.
|
||||
enabled = true
|
||||
# This is a limiter for concurrency. In order to protect website
|
||||
# from overloading, we download files in this number of threads.
|
||||
download-concurrency = 2
|
||||
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
|
||||
# You can provider links here (starts with https:// or http://) or
|
||||
# path to a local file, but in this case it should be absolute.
|
||||
urls = [
|
||||
# "https://iplists.firehol.org/files/firehol_level1.netset",
|
||||
# "/local.file"
|
||||
]
|
||||
# How often do we need to update a blocklist set.
|
||||
update-each = "24h"
|
||||
|
||||
# statsd statistics integration.
|
||||
[stats.statsd]
|
||||
# enabled/disabled
|
||||
enabled = false
|
||||
# host:port for UDP endpoint of statsd
|
||||
address = "127.0.0.1:8888"
|
||||
# prefix of metric for statsd
|
||||
metric-prefix = "mtg"
|
||||
# tag format to use
|
||||
# supported values are 'datadog', 'influxdb' and 'graphite'
|
||||
# default format is graphite.
|
||||
tag-format = "datadog"
|
||||
|
||||
# prometheus metrics integration.
|
||||
[stats.prometheus]
|
||||
# enabled/disabled
|
||||
enabled = true
|
||||
# host:port where to start http server for endpoint
|
||||
bind-to = "127.0.0.1:3129"
|
||||
# prefix of http path
|
||||
http-path = "/"
|
||||
# prefix for metrics for prometheus
|
||||
metric-prefix = "mtg"
|
||||
@@ -0,0 +1,50 @@
|
||||
module github.com/9seconds/mtg/v2
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
github.com/alecthomas/kong v0.2.17
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gotd/td v0.34.0
|
||||
github.com/jarcoal/httpmock v1.0.8
|
||||
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e
|
||||
github.com/mccutchen/go-httpbin v1.1.1
|
||||
github.com/panjf2000/ants/v2 v2.4.6
|
||||
github.com/pelletier/go-toml v1.9.4
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/common v0.31.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/smira/go-statsd v1.3.2
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/net v0.0.0-20211005001312-d4b1ae081e3b
|
||||
golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gotd/ige v0.1.5 // indirect
|
||||
github.com/gotd/xor v0.1.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.16.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
@@ -0,0 +1,574 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/alecthomas/kong v0.2.17 h1:URDISCI96MIgcIlQyoCAlhOmrSw6pZScBNkctg8r0W0=
|
||||
github.com/alecthomas/kong v0.2.17/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc=
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
|
||||
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gotd/getdoc v0.6.2/go.mod h1:6Ul8cyaq+Aw0gVyuU+TKZPCmNjdlylzSIWVxjzI5t/U=
|
||||
github.com/gotd/ige v0.1.5 h1:qITql4hZpqPM/DSeO5IVlxzXJxrs4ZZiKRwPif2K76U=
|
||||
github.com/gotd/ige v0.1.5/go.mod h1:LbvqjUuGELVuHcKjfJZ2NPNzegICQU4eqrPAkXy0nTs=
|
||||
github.com/gotd/neo v0.1.2 h1:hayFfKNSH5RP/L+KKKnsKMdDNCdhSfzVuo5TF4wvpa8=
|
||||
github.com/gotd/neo v0.1.2/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||
github.com/gotd/td v0.34.0 h1:BLIdpv2sxbCuWI2fyXCECRQuLJdnYk3VMZDRKNog/E8=
|
||||
github.com/gotd/td v0.34.0/go.mod h1:4s/7cuEscdvZIecM/pVi5L3nTK+djSoVLAhgfk40OE4=
|
||||
github.com/gotd/tl v0.4.0/go.mod h1:CMIcjPWFS4qxxJ+1Ce7U/ilbtPrkoVo/t8uhN5Y/D7c=
|
||||
github.com/gotd/xor v0.1.0/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
|
||||
github.com/gotd/xor v0.1.1 h1:LSPEeuf7noTo4fi4PrEsAaWXOSwjsY2e+IINPiR+c7s=
|
||||
github.com/gotd/xor v0.1.1/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
|
||||
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/k0kubun/pp v2.4.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
|
||||
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e h1:1wAVuGu1c+lsdaOPQN+9xoP9+gaIMJV6H0ehGc+K5iA=
|
||||
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e/go.mod h1:2OfLA+0esiUJpwMjrH39pEk79cb8MvGTBS9YlZpejJ4=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
|
||||
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/panjf2000/ants/v2 v2.4.6 h1:drmj9mcygn2gawZ155dRbo+NfXEfAssjZNU1qoIb4gQ=
|
||||
github.com/panjf2000/ants/v2 v2.4.6/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.31.1 h1:d18hG4PkHnNAKNMOmFuXFaiY8Us0nird/2m60uS1AMs=
|
||||
github.com/prometheus/common v0.31.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.2/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
|
||||
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
|
||||
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smira/go-statsd v1.3.2 h1:1EeuzxNZ/TD9apbTOFSM9nulqfcsQFmT4u1A2DREabI=
|
||||
github.com/smira/go-statsd v1.3.2/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.1.5-0.20170809224252-890a5c3458b4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg=
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
|
||||
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211005001312-d4b1ae081e3b h1:SXy8Ld8oKlcogOvUAh0J5Pm5RKzgYBMMxLxt6n5XW50=
|
||||
golang.org/x/net v0.0.0-20211005001312-d4b1ae081e3b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef h1:fPxZ3Umkct3LZ8gK9nbk+DWDJ9fstZa2grBn+lWVKPs=
|
||||
golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
@@ -0,0 +1,183 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type accessResponse struct {
|
||||
IPv4 *accessResponseURLs `json:"ipv4,omitempty"`
|
||||
IPv6 *accessResponseURLs `json:"ipv6,omitempty"`
|
||||
Secret struct {
|
||||
Hex string `json:"hex"`
|
||||
Base64 string `json:"base64"`
|
||||
} `json:"secret"`
|
||||
}
|
||||
|
||||
type accessResponseURLs struct {
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint `json:"port"`
|
||||
TgURL string `json:"tg_url"` // nolint: tagliatelle
|
||||
TgQrCode string `json:"tg_qrcode"` // nolint: tagliatelle
|
||||
TmeURL string `json:"tme_url"` // nolint: tagliatelle
|
||||
TmeQrCode string `json:"tme_qrcode"` // nolint: tagliatelle
|
||||
}
|
||||
|
||||
type Access struct {
|
||||
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
|
||||
PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` // nolint: lll
|
||||
PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` // nolint: lll
|
||||
Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` // nolint: lll
|
||||
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
|
||||
}
|
||||
|
||||
func (a *Access) Run(cli *CLI, version string) error {
|
||||
conf, err := utils.ReadConfig(a.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init config: %w", err)
|
||||
}
|
||||
|
||||
resp := &accessResponse{}
|
||||
resp.Secret.Base64 = conf.Secret.Base64()
|
||||
resp.Secret.Hex = conf.Secret.Hex()
|
||||
|
||||
ntw, err := makeNetwork(conf, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init network: %w", err)
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2) // nolint: gomnd
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ip := a.PublicIPv4
|
||||
if ip == nil {
|
||||
ip = a.getIP(ntw, "tcp4")
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
|
||||
resp.IPv4 = a.makeURLs(conf, ip)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ip := a.PublicIPv6
|
||||
if ip == nil {
|
||||
ip = a.getIP(ntw, "tcp6")
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
ip = ip.To16()
|
||||
}
|
||||
|
||||
resp.IPv6 = a.makeURLs(conf, ip)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(resp); err != nil {
|
||||
return fmt.Errorf("cannot dump access json: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
|
||||
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return ntw.DialContext(ctx, protocol, address) // nolint: wrapcheck
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "text/plain")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
io.Copy(io.Discard, resp.Body) // nolint: errcheck
|
||||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return net.ParseIP(strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
portNo := a.Port
|
||||
if portNo == 0 {
|
||||
portNo = conf.BindTo.Port
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("server", ip.String())
|
||||
values.Set("port", strconv.Itoa(int(portNo)))
|
||||
|
||||
if a.Hex {
|
||||
values.Set("secret", conf.Secret.Hex())
|
||||
} else {
|
||||
values.Set("secret", conf.Secret.Base64())
|
||||
}
|
||||
|
||||
urlQuery := values.Encode()
|
||||
|
||||
rv := &accessResponseURLs{
|
||||
IP: ip,
|
||||
Port: portNo,
|
||||
TgURL: (&url.URL{
|
||||
Scheme: "tg",
|
||||
Host: "proxy",
|
||||
RawQuery: urlQuery,
|
||||
}).String(),
|
||||
TmeURL: (&url.URL{
|
||||
Scheme: "https",
|
||||
Host: "t.me",
|
||||
Path: "proxy",
|
||||
RawQuery: urlQuery,
|
||||
}).String(),
|
||||
}
|
||||
rv.TgQrCode = utils.MakeQRCodeURL(rv.TgURL)
|
||||
rv.TmeQrCode = utils.MakeQRCodeURL(rv.TmeURL)
|
||||
|
||||
return rv
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cli
|
||||
|
||||
import "github.com/alecthomas/kong"
|
||||
|
||||
type CLI struct {
|
||||
GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"`
|
||||
Access Access `kong:"cmd,help='Print access information.'"`
|
||||
Run Run `kong:"cmd,help='Run proxy.'"`
|
||||
SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"`
|
||||
Version kong.VersionFlag `kong:"help='Print version.',short='v'"`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type GenerateSecret struct {
|
||||
HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"`
|
||||
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
|
||||
}
|
||||
|
||||
func (g *GenerateSecret) Run(cli *CLI, _ string) error {
|
||||
secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
|
||||
|
||||
if g.Hex {
|
||||
fmt.Println(secret.Hex()) // nolint: forbidigo
|
||||
} else {
|
||||
fmt.Println(secret.Base64()) // nolint: forbidigo
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
)
|
||||
|
||||
type Run struct {
|
||||
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
|
||||
}
|
||||
|
||||
func (r *Run) Run(cli *CLI, version string) error {
|
||||
conf, err := utils.ReadConfig(r.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init config: %w", err)
|
||||
}
|
||||
|
||||
return runProxy(conf, version)
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/9seconds/mtg/v2/network"
|
||||
"github.com/9seconds/mtg/v2/stats"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func makeLogger(conf *config.Config) mtglib.Logger {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFieldName = "level"
|
||||
|
||||
if conf.Debug.Get(false) {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
} else {
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
}
|
||||
|
||||
baseLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
return logger.NewZeroLogger(baseLogger)
|
||||
}
|
||||
|
||||
func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
|
||||
tcpTimeout := conf.Network.Timeout.TCP.Get(network.DefaultTimeout)
|
||||
httpTimeout := conf.Network.Timeout.HTTP.Get(network.DefaultHTTPTimeout)
|
||||
dohIP := conf.Network.DOHIP.Get(net.ParseIP(network.DefaultDOHHostname)).String()
|
||||
bufferSize := conf.TCPBuffer.Get(network.DefaultBufferSize)
|
||||
userAgent := "mtg/" + version
|
||||
|
||||
baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build a default dialer: %w", err)
|
||||
}
|
||||
|
||||
if len(conf.Network.Proxies) == 0 {
|
||||
return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
|
||||
|
||||
for _, v := range conf.Network.Proxies {
|
||||
if value := v.Get(nil); value != nil {
|
||||
proxyURLs = append(proxyURLs, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(proxyURLs) == 1 {
|
||||
socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
|
||||
}
|
||||
|
||||
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
|
||||
}
|
||||
|
||||
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
|
||||
if !conf.Defense.AntiReplay.Enabled.Get(false) {
|
||||
return antireplay.NewNoop()
|
||||
}
|
||||
|
||||
return antireplay.NewStableBloomFilter(
|
||||
conf.Defense.AntiReplay.MaxSize.Get(antireplay.DefaultStableBloomFilterMaxSize),
|
||||
conf.Defense.AntiReplay.ErrorRate.Get(antireplay.DefaultStableBloomFilterErrorRate),
|
||||
)
|
||||
}
|
||||
|
||||
func makeIPBlocklist(conf *config.Config, logger mtglib.Logger, ntw mtglib.Network) (mtglib.IPBlocklist, error) {
|
||||
if !conf.Defense.Blocklist.Enabled.Get(false) {
|
||||
return ipblocklist.NewNoop(), nil
|
||||
}
|
||||
|
||||
remoteURLs := []string{}
|
||||
localFiles := []string{}
|
||||
|
||||
for _, v := range conf.Defense.Blocklist.URLs {
|
||||
if v.IsRemote() {
|
||||
remoteURLs = append(remoteURLs, v.String())
|
||||
} else {
|
||||
localFiles = append(localFiles, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
firehol, err := ipblocklist.NewFirehol(logger.Named("ipblockist"),
|
||||
ntw,
|
||||
conf.Defense.Blocklist.DownloadConcurrency.Get(1),
|
||||
remoteURLs,
|
||||
localFiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("incorrect parameters for firehol: %w", err)
|
||||
}
|
||||
|
||||
return firehol, nil
|
||||
}
|
||||
|
||||
func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) {
|
||||
factories := make([]events.ObserverFactory, 0, 2) // nolint: gomnd
|
||||
|
||||
if conf.Stats.StatsD.Enabled.Get(false) {
|
||||
statsdFactory, err := stats.NewStatsd(
|
||||
conf.Stats.StatsD.Address.Get(""),
|
||||
logger.Named("statsd"),
|
||||
conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix),
|
||||
conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build statsd observer: %w", err)
|
||||
}
|
||||
|
||||
factories = append(factories, statsdFactory.Make)
|
||||
}
|
||||
|
||||
if conf.Stats.Prometheus.Enabled.Get(false) {
|
||||
prometheus := stats.NewPrometheus(
|
||||
conf.Stats.Prometheus.MetricPrefix.Get(stats.DefaultMetricPrefix),
|
||||
conf.Stats.Prometheus.HTTPPath.Get("/"),
|
||||
)
|
||||
|
||||
listener, err := net.Listen("tcp", conf.Stats.Prometheus.BindTo.Get(""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot start a listener for prometheus: %w", err)
|
||||
}
|
||||
|
||||
go prometheus.Serve(listener) // nolint: errcheck
|
||||
|
||||
factories = append(factories, prometheus.Make)
|
||||
}
|
||||
|
||||
if len(factories) > 0 {
|
||||
return events.NewEventStream(factories), nil
|
||||
}
|
||||
|
||||
return events.NewNoopStream(), nil
|
||||
}
|
||||
|
||||
func runProxy(conf *config.Config, version string) error {
|
||||
logger := makeLogger(conf)
|
||||
|
||||
logger.BindJSON("configuration", conf.String()).Debug("configuration")
|
||||
|
||||
ntw, err := makeNetwork(conf, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build network: %w", err)
|
||||
}
|
||||
|
||||
blocklist, err := makeIPBlocklist(conf, logger, ntw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build ip blocklist: %w", err)
|
||||
}
|
||||
|
||||
eventStream, err := makeEventStream(conf, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build event stream: %w", err)
|
||||
}
|
||||
|
||||
opts := mtglib.ProxyOpts{
|
||||
Logger: logger,
|
||||
Network: ntw,
|
||||
AntiReplayCache: makeAntiReplayCache(conf),
|
||||
IPBlocklist: blocklist,
|
||||
EventStream: eventStream,
|
||||
|
||||
Secret: conf.Secret,
|
||||
BufferSize: conf.TCPBuffer.Get(mtglib.DefaultBufferSize),
|
||||
DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort),
|
||||
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
|
||||
|
||||
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
|
||||
}
|
||||
|
||||
proxy, err := mtglib.NewProxy(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create a proxy: %w", err)
|
||||
}
|
||||
|
||||
listener, err := utils.NewListener(conf.BindTo.Get(""), int(opts.BufferSize))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start proxy: %w", err)
|
||||
}
|
||||
|
||||
ctx := utils.RootContext()
|
||||
|
||||
go proxy.Serve(listener) // nolint: errcheck
|
||||
|
||||
<-ctx.Done()
|
||||
listener.Close()
|
||||
proxy.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
)
|
||||
|
||||
type SimpleRun struct {
|
||||
BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
|
||||
Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`
|
||||
|
||||
Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` // nolint: lll
|
||||
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` // nolint: lll
|
||||
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Size of TCP buffer to use.'"` // nolint: lll
|
||||
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` // nolint: lll
|
||||
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` // nolint: lll
|
||||
DOHIP net.IP `kong:"name='doh-ip',short='n',default='9.9.9.9',help='IP address of DNS-over-HTTP to use.'"` // nolint: lll
|
||||
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` // nolint: lll
|
||||
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` // nolint: lll
|
||||
}
|
||||
|
||||
func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop
|
||||
conf := &config.Config{}
|
||||
|
||||
if err := conf.BindTo.Set(s.BindTo); err != nil {
|
||||
return fmt.Errorf("incorrect bind-to parameter: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Secret.Set(s.Secret); err != nil {
|
||||
return fmt.Errorf("incorrect secret: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Concurrency.Set(strconv.FormatUint(s.Concurrency, 10)); err != nil { // nolint: gomnd
|
||||
return fmt.Errorf("incorrect concurrency: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.TCPBuffer.Set(s.TCPBuffer); err != nil {
|
||||
return fmt.Errorf("incorrect tcp-buffer: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.PreferIP.Set(s.PreferIP); err != nil {
|
||||
return fmt.Errorf("incorrect prefer-ip: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.DomainFrontingPort.Set(strconv.FormatUint(s.DomainFrontingPort, 10)); err != nil { // nolint: gomnd
|
||||
return fmt.Errorf("incorrect domain-fronting-port: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.DOHIP.Set(s.DOHIP.String()); err != nil {
|
||||
return fmt.Errorf("incorrect doh-ip: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.Timeout.TCP.Set(s.Timeout.String()); err != nil {
|
||||
return fmt.Errorf("incorrect timeout: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.Timeout.HTTP.Set(s.Timeout.String()); err != nil {
|
||||
return fmt.Errorf("incorrect timeout: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.Timeout.Idle.Set(s.Timeout.String()); err != nil {
|
||||
return fmt.Errorf("incorrect timeout: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Defense.AntiReplay.MaxSize.Set(s.AntiReplayCacheSize); err != nil {
|
||||
return fmt.Errorf("incorrect antireplay-cache-size: %w", err)
|
||||
}
|
||||
|
||||
conf.Debug.Value = s.Debug
|
||||
conf.AllowFallbackOnUnknownDC.Value = true
|
||||
conf.Defense.AntiReplay.Enabled.Value = true
|
||||
|
||||
if err := conf.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid result configuration: %w", err)
|
||||
}
|
||||
|
||||
return runProxy(conf, version)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Debug TypeBool `json:"debug"`
|
||||
AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"`
|
||||
Secret mtglib.Secret `json:"secret"`
|
||||
BindTo TypeHostPort `json:"bindTo"`
|
||||
TCPBuffer TypeBytes `json:"tcpBuffer"`
|
||||
PreferIP TypePreferIP `json:"preferIp"`
|
||||
DomainFrontingPort TypePort `json:"domainFrontingPort"`
|
||||
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
|
||||
Concurrency TypeConcurrency `json:"concurrency"`
|
||||
Defense struct {
|
||||
AntiReplay struct {
|
||||
Enabled TypeBool `json:"enabled"`
|
||||
MaxSize TypeBytes `json:"maxSize"`
|
||||
ErrorRate TypeErrorRate `json:"errorRate"`
|
||||
} `json:"antiReplay"`
|
||||
Blocklist struct {
|
||||
Enabled TypeBool `json:"enabled"`
|
||||
DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"`
|
||||
URLs []TypeBlocklistURI `json:"urls"`
|
||||
UpdateEach TypeDuration `json:"updateEach"`
|
||||
} `json:"blocklist"`
|
||||
} `json:"defense"`
|
||||
Network struct {
|
||||
Timeout struct {
|
||||
TCP TypeDuration `json:"tcp"`
|
||||
HTTP TypeDuration `json:"http"`
|
||||
Idle TypeDuration `json:"idle"`
|
||||
} `json:"timeout"`
|
||||
DOHIP TypeIP `json:"dohIp"`
|
||||
Proxies []TypeProxyURL `json:"proxies"`
|
||||
} `json:"network"`
|
||||
Stats struct {
|
||||
StatsD struct {
|
||||
Enabled TypeBool `json:"enabled"`
|
||||
Address TypeHostPort `json:"address"`
|
||||
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||
TagFormat TypeStatsdTagFormat `json:"tagFormat"`
|
||||
} `json:"statsd"`
|
||||
Prometheus struct {
|
||||
Enabled TypeBool `json:"enabled"`
|
||||
BindTo TypeHostPort `json:"bindTo"`
|
||||
HTTPPath TypeHTTPPath `json:"httpPath"`
|
||||
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||
} `json:"prometheus"`
|
||||
} `json:"stats"`
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if !c.Secret.Valid() {
|
||||
return fmt.Errorf("invalid secret %s", c.Secret.String())
|
||||
}
|
||||
|
||||
if c.BindTo.Get("") == "" {
|
||||
return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) String() string {
|
||||
buf := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buf)
|
||||
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
if err := encoder.Encode(c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ConfigTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) ReadConfig(filename string) []byte {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", filename))
|
||||
suite.NoError(err)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseEmpty() {
|
||||
_, err := config.Parse([]byte{})
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseBrokenToml() {
|
||||
_, err := config.Parse(suite.ReadConfig("broken.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseOnlySecret() {
|
||||
_, err := config.Parse(suite.ReadConfig("only_secret.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseMinimalConfig() {
|
||||
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||
suite.NoError(err)
|
||||
suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64())
|
||||
suite.Equal("0.0.0.0:3128", conf.BindTo.String())
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestString() {
|
||||
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(conf.String())
|
||||
}
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ConfigTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
type tomlConfig struct {
|
||||
Debug bool `toml:"debug" json:"debug,omitempty"`
|
||||
AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"`
|
||||
Secret string `toml:"secret" json:"secret"`
|
||||
BindTo string `toml:"bind-to" json:"bindTo"`
|
||||
TCPBuffer string `toml:"tcp-buffer" json:"tcpBuffer,omitempty"`
|
||||
PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"`
|
||||
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`
|
||||
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
|
||||
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
|
||||
Defense struct {
|
||||
AntiReplay struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
MaxSize string `toml:"max-size" json:"maxSize,omitempty"`
|
||||
ErrorRate float64 `toml:"error-rate" json:"errorRate,omitempty"`
|
||||
} `toml:"anti-replay" json:"antiReplay,omitempty"`
|
||||
Blocklist struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"`
|
||||
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
|
||||
} `toml:"blocklist" json:"blocklist,omitempty"`
|
||||
} `toml:"defense" json:"defense,omitempty"`
|
||||
Network struct {
|
||||
Timeout struct {
|
||||
TCP string `toml:"tcp" json:"tcp,omitempty"`
|
||||
HTTP string `toml:"http" json:"http,omitempty"`
|
||||
Idle string `toml:"idle" json:"idle,omitempty"`
|
||||
} `toml:"timeout" json:"timeout,omitempty"`
|
||||
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
|
||||
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
|
||||
} `toml:"network" json:"network,omitempty"`
|
||||
Stats struct {
|
||||
StatsD struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
Address string `toml:"address" json:"address,omitempty"`
|
||||
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||
TagFormat string `toml:"tag-format" json:"tagFormat,omitempty"`
|
||||
} `toml:"statsd" json:"statsd,omitempty"`
|
||||
Prometheus struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
BindTo string `toml:"bind-to" json:"bindTo,omitempty"`
|
||||
HTTPPath string `toml:"http-path" json:"httpPath,omitempty"`
|
||||
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||
} `toml:"prometheus" json:"prometheus,omitempty"`
|
||||
} `toml:"stats" json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
func Parse(rawData []byte) (*Config, error) {
|
||||
tomlConf := &tomlConfig{}
|
||||
jsonBuf := &bytes.Buffer{}
|
||||
conf := &Config{}
|
||||
|
||||
jsonEncoder := json.NewEncoder(jsonBuf)
|
||||
jsonEncoder.SetEscapeHTML(false)
|
||||
jsonEncoder.SetIndent("", "")
|
||||
|
||||
if err := toml.Unmarshal(rawData, tomlConf); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse toml config: %w", err)
|
||||
}
|
||||
|
||||
if err := jsonEncoder.Encode(tomlConf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse a config: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
s = sdfsdfds
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||
bind-to = "0.0.0.0:3128"
|
||||
+1
@@ -0,0 +1 @@
|
||||
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||
@@ -0,0 +1,77 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type TypeBlocklistURI struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeBlocklistURI) Set(value string) error {
|
||||
if stat, err := os.Stat(value); err == nil || os.IsExist(err) {
|
||||
switch {
|
||||
case stat.IsDir():
|
||||
return fmt.Errorf("value is correct filepath but directory")
|
||||
case stat.Mode().Perm()&0o400 == 0:
|
||||
return fmt.Errorf("value is correct filepath but not readable")
|
||||
}
|
||||
|
||||
value, err = filepath.Abs(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"value is correct filepath but cannot resolve absolute (%s): %w",
|
||||
value, err)
|
||||
}
|
||||
|
||||
t.Value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect url (%s): %w", value, err)
|
||||
}
|
||||
|
||||
switch parsedURL.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value)
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("incorrect url %s", value)
|
||||
}
|
||||
|
||||
t.Value = parsedURL.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) IsRemote() bool {
|
||||
return !filepath.IsAbs(t.Value)
|
||||
}
|
||||
|
||||
func (t *TypeBlocklistURI) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeBlocklistURITestStruct struct {
|
||||
Value config.TypeBlocklistURI `json:"value"`
|
||||
}
|
||||
|
||||
type TypeBlocklistURITestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
directory string
|
||||
absDirectory string
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) SetupSuite() {
|
||||
dir, _ := os.Getwd()
|
||||
absDir, _ := filepath.Abs(dir)
|
||||
|
||||
suite.directory = dir
|
||||
suite.absDirectory = absDir
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"gopher://lalala",
|
||||
"https:///paths",
|
||||
"h:/=",
|
||||
filepath.Join(suite.directory, "___"),
|
||||
filepath.Join(suite.absDirectory, "___"),
|
||||
suite.directory,
|
||||
suite.absDirectory,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
"http://lalala",
|
||||
"https://lalala",
|
||||
"https://lalala/path",
|
||||
filepath.Join(suite.directory, "config.go"),
|
||||
filepath.Join(suite.absDirectory, "config.go"),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeBlocklistURITestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.EqualValues(t, value, testStruct.Value.Get(""))
|
||||
|
||||
if strings.HasPrefix(value, "http") {
|
||||
assert.True(t, testStruct.Value.IsRemote())
|
||||
} else {
|
||||
assert.False(t, testStruct.Value.IsRemote())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestMarshalOk() {
|
||||
testStruct := &typeBlocklistURITestStruct{
|
||||
Value: config.TypeBlocklistURI{
|
||||
Value: "http://some.url/with/path",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "http://some.url/with/path"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestGet() {
|
||||
value := config.TypeBlocklistURI{}
|
||||
suite.Equal("/path", value.Get("/path"))
|
||||
|
||||
suite.NoError(value.Set("http://lalala.ru"))
|
||||
suite.Equal("http://lalala.ru", value.Get("/path"))
|
||||
suite.Equal("http://lalala.ru", value.Get(""))
|
||||
}
|
||||
|
||||
func TestTypeBlocklistURI(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBlocklistURITestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeBool struct {
|
||||
Value bool
|
||||
}
|
||||
|
||||
func (t *TypeBool) Set(data string) error {
|
||||
parsed, err := strconv.ParseBool(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect bool value: %s", data)
|
||||
}
|
||||
|
||||
t.Value = parsed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeBool) Get(defaultValue bool) bool {
|
||||
return t.Value || defaultValue
|
||||
}
|
||||
|
||||
func (t *TypeBool) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeBool) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeBool) String() string {
|
||||
return strconv.FormatBool(t.Value)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeBoolTestStruct struct {
|
||||
Value config.TypeBool `json:"value"`
|
||||
}
|
||||
|
||||
type TypeBoolTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestUnmarshalFail() {
|
||||
testData := []interface{}{
|
||||
"",
|
||||
"np",
|
||||
"нет",
|
||||
int(10),
|
||||
[]int{},
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]interface{}{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(fmt.Sprintf("%v", v), func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestUnmarshalOk() {
|
||||
testData := []bool{
|
||||
true,
|
||||
false,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]bool{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.FormatBool(v), func(t *testing.T) {
|
||||
testStruct := &typeBoolTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
|
||||
if value {
|
||||
assert.True(t, testStruct.Value.Value)
|
||||
} else {
|
||||
assert.False(t, testStruct.Value.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestMarshalOk() {
|
||||
for _, v := range []bool{true, false} {
|
||||
value := v
|
||||
|
||||
suite.T().Run(strconv.FormatBool(v), func(t *testing.T) {
|
||||
testStruct := typeBoolTestStruct{
|
||||
Value: config.TypeBool{
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]bool{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestGet() {
|
||||
value := config.TypeBool{}
|
||||
suite.False(value.Get(false))
|
||||
suite.True(value.Get(true))
|
||||
|
||||
value.Value = true
|
||||
suite.True(value.Get(false))
|
||||
suite.True(value.Get(true))
|
||||
}
|
||||
|
||||
func TestTypeBool(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBoolTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
)
|
||||
|
||||
var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB")
|
||||
|
||||
type TypeBytes struct {
|
||||
Value units.Base2Bytes
|
||||
}
|
||||
|
||||
func (t *TypeBytes) Set(value string) error {
|
||||
normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value))
|
||||
|
||||
parsedValue, err := units.ParseBase2Bytes(normalizedValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect bytes value (%v): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedValue < 0 {
|
||||
return fmt.Errorf("bytes should be positive (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = parsedValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeBytes) Get(defaultValue uint) uint {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return uint(t.Value)
|
||||
}
|
||||
|
||||
func (t *TypeBytes) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeBytes) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeBytes) String() string {
|
||||
if t.Value == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(t.Value.String())
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeBytesTestStruct struct {
|
||||
Value config.TypeBytes `json:"value"`
|
||||
}
|
||||
|
||||
type TypeBytesTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"1m",
|
||||
"1",
|
||||
"-1kb",
|
||||
"-1kib",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]uint{
|
||||
"1b": 1,
|
||||
"1kb": 1024,
|
||||
"1kib": 1024,
|
||||
"2mb": 2 * 1024 * 1024,
|
||||
"2mib": 2 * 1024 * 1024,
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeBytesTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.EqualValues(t, value, testStruct.Value.Get(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestMarshalOk() {
|
||||
value := typeBytesTestStruct{}
|
||||
suite.NoError(value.Value.Set("1kib"))
|
||||
|
||||
data, err := json.Marshal(value)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "1kib"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestGet() {
|
||||
value := config.TypeBytes{}
|
||||
suite.EqualValues(1000, value.Get(1000))
|
||||
|
||||
suite.NoError(value.Set("1mib"))
|
||||
suite.EqualValues(1048576, value.Get(1000))
|
||||
}
|
||||
|
||||
func TestTypeBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBytesTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeConcurrency struct {
|
||||
Value uint
|
||||
}
|
||||
|
||||
func (t *TypeConcurrency) Set(value string) error {
|
||||
concurrencyValue, err := strconv.ParseUint(value, 10, 16) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not uint (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if concurrencyValue == 0 {
|
||||
return fmt.Errorf("value should be >0 (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = uint(concurrencyValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeConcurrency) Get(defaultValue uint) uint {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeConcurrency) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeConcurrency) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeConcurrency) String() string {
|
||||
return strconv.FormatUint(uint64(t.Value), 10) // nolint: gomnd
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeConcurrencyTestStruct struct {
|
||||
Value config.TypeConcurrency `json:"value"`
|
||||
}
|
||||
|
||||
type TypeConcurrencyTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"-1",
|
||||
"0",
|
||||
"0.0",
|
||||
"1.0",
|
||||
"1.1",
|
||||
".",
|
||||
"some_value",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeConcurrencyTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() {
|
||||
testStruct := &typeConcurrencyTestStruct{}
|
||||
|
||||
suite.NoError(json.Unmarshal([]byte(`{"value": 1}`), testStruct))
|
||||
suite.EqualValues(1, testStruct.Value.Get(2))
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestMarshalOk() {
|
||||
testStruct := &typeConcurrencyTestStruct{
|
||||
Value: config.TypeConcurrency{
|
||||
Value: 2,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": 2}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestGet() {
|
||||
value := config.TypeConcurrency{}
|
||||
suite.EqualValues(1, value.Get(1))
|
||||
|
||||
value.Value = 3
|
||||
suite.EqualValues(3, value.Get(1))
|
||||
}
|
||||
|
||||
func TestTypeConcurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeConcurrencyTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "")
|
||||
|
||||
type TypeDuration struct {
|
||||
Value time.Duration
|
||||
}
|
||||
|
||||
func (t *TypeDuration) Set(value string) error {
|
||||
parsedValue, err := time.ParseDuration(
|
||||
typeDurationStringCleaner.Replace(strings.ToLower(value)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect duration (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedValue < 0 {
|
||||
return fmt.Errorf("duration has to be a positive: %s", value)
|
||||
}
|
||||
|
||||
t.Value = parsedValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeDuration) Get(defaultValue time.Duration) time.Duration {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeDuration) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeDuration) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeDuration) String() string {
|
||||
if t.Value == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Value.String()
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeDurationTestStruct struct {
|
||||
Value config.TypeDuration `json:"value"`
|
||||
}
|
||||
|
||||
type TypeDurationTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"-1s",
|
||||
"1 seconds ago",
|
||||
"1s ago",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeDurationTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]time.Duration{
|
||||
"1s": time.Second,
|
||||
"0": 0 * time.Second,
|
||||
"0s": 0 * time.Second,
|
||||
"1\tM": time.Minute,
|
||||
"1H": time.Hour,
|
||||
"1 h": time.Hour,
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeDurationTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestMarshalOk() {
|
||||
testData := map[string]string{
|
||||
"1s": "1s",
|
||||
"0": "",
|
||||
"0s": "",
|
||||
"0ms": "",
|
||||
"1 H": "1h0m0s",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := k
|
||||
expected := v
|
||||
|
||||
suite.T().Run(value, func(t *testing.T) {
|
||||
testStruct := &typeDurationTestStruct{}
|
||||
|
||||
assert.NoError(t, testStruct.Value.Set(value))
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": expected,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestGet() {
|
||||
value := config.TypeDuration{}
|
||||
suite.Equal(time.Second, value.Get(time.Second))
|
||||
|
||||
value.Value = 3 * time.Second
|
||||
suite.Equal(3*time.Second, value.Get(time.Hour))
|
||||
}
|
||||
|
||||
func TestTypeDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeDurationTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const typeErrorRateIgnoreLess = 1e-8
|
||||
|
||||
type TypeErrorRate struct {
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (t *TypeErrorRate) Set(value string) error {
|
||||
parsedValue, err := strconv.ParseFloat(value, 64) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not a float (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedValue <= 0.0 || parsedValue >= 100.0 {
|
||||
return fmt.Errorf("value should be 0 < x < 100 (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = parsedValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeErrorRate) Get(defaultValue float64) float64 {
|
||||
if t.Value < typeErrorRateIgnoreLess {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeErrorRate) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeErrorRate) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeErrorRate) String() string {
|
||||
return strconv.FormatFloat(t.Value, 'f', -1, 64) // nolint: gomnd
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeErrorRateTestStruct struct {
|
||||
Value config.TypeErrorRate `json:"value"`
|
||||
}
|
||||
|
||||
type TypeErrorRateTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"1s",
|
||||
"1,",
|
||||
"1,2",
|
||||
".",
|
||||
"3.4.5",
|
||||
"3.5.",
|
||||
".3.5",
|
||||
"some word",
|
||||
"1e2",
|
||||
"-1.0",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() {
|
||||
data, err := json.Marshal(map[string]float64{
|
||||
"value": 1.0,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
testStruct := &typeErrorRateTestStruct{}
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
suite.InEpsilon(1.0, testStruct.Value.Value, 1e-10)
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestMarshalOk() {
|
||||
testStruct := typeErrorRateTestStruct{
|
||||
Value: config.TypeErrorRate{
|
||||
Value: 1.01,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": 1.01}`, string(encodedJSON))
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestGet() {
|
||||
value := config.TypeErrorRate{}
|
||||
suite.InEpsilon(1.0, value.Get(1.0), 1e-10)
|
||||
|
||||
value.Value = 5.0
|
||||
suite.InEpsilon(5.0, value.Get(1.0), 1e-10)
|
||||
}
|
||||
|
||||
func TestTypeErrorRate(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeErrorRateTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeHostPort struct {
|
||||
Value string
|
||||
Host string
|
||||
Port uint
|
||||
}
|
||||
|
||||
func (t *TypeHostPort) Set(value string) error {
|
||||
host, port, err := net.SplitHostPort(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect host:port value (%v): %w", value, err)
|
||||
}
|
||||
|
||||
portValue, err := strconv.ParseUint(port, 10, 16) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect port number (%v): %w", value, err)
|
||||
}
|
||||
|
||||
if portValue == 0 {
|
||||
return fmt.Errorf("incorrect port number (%s)", value)
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return fmt.Errorf("empty host: %s", value)
|
||||
}
|
||||
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("host is not an IP address: %s", value)
|
||||
}
|
||||
|
||||
t.Value = net.JoinHostPort(host, port)
|
||||
t.Port = uint(portValue)
|
||||
t.Host = host
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeHostPort) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeHostPort) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeHostPort) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeHostPort) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeHostPortTestStruct struct {
|
||||
Value config.TypeHostPort `json:"value"`
|
||||
}
|
||||
|
||||
type TypeHostPortTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
":",
|
||||
":800",
|
||||
"127.0.0.1:8000000",
|
||||
"12...:80",
|
||||
"",
|
||||
"localhost",
|
||||
"google.com:",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeHostPortTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
"127.0.0.1:80",
|
||||
"10.0.0.10:6553",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeHostPortTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestMarshalOk() {
|
||||
testStruct := typeHostPortTestStruct{
|
||||
Value: config.TypeHostPort{
|
||||
Value: "127.0.0.1:8000",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestGet() {
|
||||
value := config.TypeHostPort{}
|
||||
suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000"))
|
||||
|
||||
value.Value = "127.0.0.1:80"
|
||||
suite.Equal("127.0.0.1:80", value.Get("127.0.0.1:9000"))
|
||||
}
|
||||
|
||||
func TestTypeHostPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeHostPortTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
type TypeHTTPPath struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeHTTPPath) Set(value string) error {
|
||||
t.Value = "/" + strings.Trim(value, "/")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeHTTPPath) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeHTTPPath) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeHTTPPath) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeHTTPPath) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeHTTPPathTestStruct struct {
|
||||
Value config.TypeHTTPPath `json:"value"`
|
||||
}
|
||||
|
||||
type TypeHTTPPathTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"": "/",
|
||||
"/": "/",
|
||||
"/path": "/path",
|
||||
"path": "/path",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeHTTPPathTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Get(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestMarshalOk() {
|
||||
value := typeHTTPPathTestStruct{
|
||||
Value: config.TypeHTTPPath{
|
||||
Value: "/path",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(value)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "/path"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestGet() {
|
||||
value := config.TypeHTTPPath{}
|
||||
suite.Equal("/hello", value.Get("/hello"))
|
||||
|
||||
suite.NoError(value.Set("/lalala"))
|
||||
suite.Equal("/lalala", value.Get("/hello"))
|
||||
}
|
||||
|
||||
func TestTypeHTTPPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeHTTPPathTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type TypeIP struct {
|
||||
Value net.IP
|
||||
}
|
||||
|
||||
func (t *TypeIP) Set(value string) error {
|
||||
ip := net.ParseIP(value)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("incorret ip %s", value)
|
||||
}
|
||||
|
||||
t.Value = ip
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TypeIP) Get(defaultValue net.IP) net.IP {
|
||||
if len(t.Value) == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeIP) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeIP) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeIP) String() string {
|
||||
if len(t.Value) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Value.String()
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeIPTestStruct struct {
|
||||
Value config.TypeIP `json:"value"`
|
||||
}
|
||||
|
||||
type TypeIPTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"....",
|
||||
"0...",
|
||||
"300.200.200.800",
|
||||
"[]",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeIPTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "2001:db8:85a3::8a2e:370:7334",
|
||||
"127.0.0.1": "127.0.0.1",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
expected := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeIPTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, expected, testStruct.Value.Get(nil).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"2001:db8:85a3::8a2e:370:7334",
|
||||
"127.0.0.1",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeIPTestStruct{
|
||||
Value: config.TypeIP{
|
||||
Value: net.ParseIP(value),
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestGet() {
|
||||
value := config.TypeIP{}
|
||||
suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).String())
|
||||
|
||||
suite.NoError(value.Set("127.0.0.2"))
|
||||
suite.Equal("127.0.0.2", value.Get(net.ParseIP("127.0.0.1")).String())
|
||||
}
|
||||
|
||||
func TestTypeIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeIPTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type TypeMetricPrefix struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeMetricPrefix) Set(value string) error {
|
||||
if ok, err := regexp.MatchString("^[a-z0-9]+$", value); !ok || err != nil {
|
||||
return fmt.Errorf("incorrect metric prefix %s: %w", value, err)
|
||||
}
|
||||
|
||||
t.Value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeMetricPrefix) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeMetricPrefix) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeMetricPrefix) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeMetricPrefix) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeMetricPrefixTestStruct struct {
|
||||
Value config.TypeMetricPrefix `json:"value"`
|
||||
}
|
||||
|
||||
type TypeMetricPrefixTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"-",
|
||||
"hello/world",
|
||||
"lala*",
|
||||
"++sdf++",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeMetricPrefixTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() {
|
||||
testStruct := &typeMetricPrefixTestStruct{}
|
||||
suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct))
|
||||
suite.Equal("mtg", testStruct.Value.Get("lalala"))
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() {
|
||||
testStruct := &typeMetricPrefixTestStruct{
|
||||
Value: config.TypeMetricPrefix{
|
||||
Value: "mtg",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "mtg"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestGet() {
|
||||
value := config.TypeMetricPrefix{}
|
||||
suite.Equal("lalala", value.Get("lalala"))
|
||||
|
||||
value.Value = "mtg"
|
||||
suite.Equal("mtg", value.Get("lalala"))
|
||||
}
|
||||
|
||||
func TestTypeMetricPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeMetricPrefixTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypePort struct {
|
||||
Value uint
|
||||
}
|
||||
|
||||
func (t *TypePort) Set(value string) error {
|
||||
portValue, err := strconv.ParseUint(value, 10, 16) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect port number (%v): %w", value, err)
|
||||
}
|
||||
|
||||
if portValue == 0 {
|
||||
return fmt.Errorf("incorrect port number (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = uint(portValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypePort) Get(defaultValue uint) uint {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypePort) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypePort) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypePort) String() string {
|
||||
return strconv.Itoa(int(t.Value))
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typePortTestStruct struct {
|
||||
Value config.TypePort `json:"value"`
|
||||
}
|
||||
|
||||
type TypePortTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"port",
|
||||
"0",
|
||||
"-1",
|
||||
"1.5",
|
||||
"70000",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typePortTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalOk() {
|
||||
testStruct := &typePortTestStruct{}
|
||||
suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct))
|
||||
suite.EqualValues(5, testStruct.Value.Value)
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestMarshalOk() {
|
||||
testStruct := &typePortTestStruct{
|
||||
Value: config.TypePort{
|
||||
Value: 10,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value":10}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestGet() {
|
||||
value := config.TypePort{}
|
||||
suite.EqualValues(10, value.Get(10))
|
||||
|
||||
value.Value = 100
|
||||
suite.EqualValues(100, value.Get(10))
|
||||
}
|
||||
|
||||
func TestTypePort(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypePortTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypePreferIPPreferIPv4 states that you prefer to use IPv4 addresses
|
||||
// but IPv6 is also possible.
|
||||
TypePreferIPPreferIPv4 = "prefer-ipv4"
|
||||
|
||||
// TypePreferIPPreferIPv6 states that you prefer to use IPv6 addresses
|
||||
// but IPv4 is also possible.
|
||||
TypePreferIPPreferIPv6 = "prefer-ipv6"
|
||||
|
||||
// TypePreferOnlyIPv4 states that you prefer to use IPv4 addresses
|
||||
// only.
|
||||
TypePreferOnlyIPv4 = "only-ipv4"
|
||||
|
||||
// TypePreferOnlyIPv6 states that you prefer to use IPv6 addresses
|
||||
// only.
|
||||
TypePreferOnlyIPv6 = "only-ipv6"
|
||||
)
|
||||
|
||||
type TypePreferIP struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypePreferIP) Set(value string) error {
|
||||
value = strings.ToLower(value)
|
||||
|
||||
switch value {
|
||||
case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6,
|
||||
TypePreferOnlyIPv4, TypePreferOnlyIPv6:
|
||||
t.Value = value
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported ip preference: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TypePreferIP) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypePreferIP) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypePreferIP) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypePreferIP) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typePreferIPTestStruct struct {
|
||||
Value config.TypePreferIP `json:"value"`
|
||||
}
|
||||
|
||||
type TypePreferIPTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"prefer",
|
||||
"preferipv4",
|
||||
config.TypePreferIPPreferIPv4 + "_",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typePreferIPTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
config.TypePreferIPPreferIPv4,
|
||||
config.TypePreferIPPreferIPv6,
|
||||
config.TypePreferOnlyIPv4,
|
||||
config.TypePreferOnlyIPv6,
|
||||
strings.ToTitle(config.TypePreferOnlyIPv4),
|
||||
strings.ToTitle(config.TypePreferOnlyIPv6),
|
||||
strings.ToTitle(config.TypePreferIPPreferIPv4),
|
||||
strings.ToTitle(config.TypePreferIPPreferIPv6),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typePreferIPTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, strings.ToLower(value), testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
config.TypePreferIPPreferIPv4,
|
||||
config.TypePreferIPPreferIPv6,
|
||||
config.TypePreferOnlyIPv4,
|
||||
config.TypePreferOnlyIPv6,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typePreferIPTestStruct{
|
||||
Value: config.TypePreferIP{
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestGet() {
|
||||
value := config.TypePreferIP{}
|
||||
suite.Equal(config.TypePreferIPPreferIPv4,
|
||||
value.Get(config.TypePreferIPPreferIPv4))
|
||||
|
||||
suite.NoError(value.Set(config.TypePreferIPPreferIPv6))
|
||||
suite.Equal(config.TypePreferIPPreferIPv6,
|
||||
value.Get(config.TypePreferIPPreferIPv4))
|
||||
}
|
||||
|
||||
func TestTypePreferIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypePreferIPTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const typeProxyURLDefaultSOCKS5Port = "1080"
|
||||
|
||||
type TypeProxyURL struct {
|
||||
Value *url.URL
|
||||
}
|
||||
|
||||
func (t *TypeProxyURL) Set(value string) error {
|
||||
parsedURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not corect URL (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("url has to have a schema: %s", value)
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "socks5" {
|
||||
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil {
|
||||
parsedURL.Host = net.JoinHostPort(parsedURL.Host,
|
||||
typeProxyURLDefaultSOCKS5Port)
|
||||
}
|
||||
|
||||
t.Value = parsedURL
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TypeProxyURL) Get(defaultValue *url.URL) *url.URL {
|
||||
if t.Value == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeProxyURL) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeProxyURL) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeProxyURL) String() string {
|
||||
if t.Value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Value.String()
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeProxyURLTestStruct struct {
|
||||
Value config.TypeProxyURL `json:"value"`
|
||||
}
|
||||
|
||||
type ProxyURLTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"socks5://",
|
||||
"://lala",
|
||||
"/path",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeProxyURLTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"socks5://127.0.0.1/?open_threshold=1": "socks5://127.0.0.1:1080/?open_threshold=1",
|
||||
"socks5://127.0.0.1:80": "socks5://127.0.0.1:80",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeProxyURLTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
|
||||
parsed, _ := url.Parse(value)
|
||||
|
||||
assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme)
|
||||
assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host)
|
||||
assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery)
|
||||
assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestMarshalOk() {
|
||||
parsed, _ := url.Parse("socks5://127.0.0.1:1080?open_threshold=1")
|
||||
testStruct := &typeProxyURLTestStruct{
|
||||
Value: config.TypeProxyURL{
|
||||
Value: parsed,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "socks5://127.0.0.1:1080?open_threshold=1"}`,
|
||||
string(encodedJSON))
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestGet() {
|
||||
emptyURL := &url.URL{}
|
||||
|
||||
value := config.TypeProxyURL{}
|
||||
suite.Equal(emptyURL, value.Get(emptyURL))
|
||||
|
||||
value.Value = &url.URL{}
|
||||
suite.Equal(value.Value, value.Get(emptyURL))
|
||||
}
|
||||
|
||||
func TestTypeProxyURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ProxyURLTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeStatsdTagFormatInfluxdb defines a tag format compatible with
|
||||
// InfluxDB.
|
||||
TypeStatsdTagFormatInfluxdb = "influxdb"
|
||||
|
||||
// TypeStatsdTagFormatDatadog defines a tag format compatible with
|
||||
// DataDog.
|
||||
TypeStatsdTagFormatDatadog = "datadog"
|
||||
|
||||
// TypeStatsdTagFormatGraphite defines a tag format compatible with
|
||||
// Graphite.
|
||||
TypeStatsdTagFormatGraphite = "graphite"
|
||||
)
|
||||
|
||||
type TypeStatsdTagFormat struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) Set(value string) error {
|
||||
lowercasedValue := strings.ToLower(value)
|
||||
|
||||
switch lowercasedValue {
|
||||
case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb,
|
||||
TypeStatsdTagFormatGraphite:
|
||||
t.Value = lowercasedValue
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown tag format %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (t TypeStatsdTagFormat) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeStatsdTagFormatTestStruct struct {
|
||||
Value config.TypeStatsdTagFormat `json:"value"`
|
||||
}
|
||||
|
||||
type StatsdTagFormatTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"dogdog",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
config.TypeStatsdTagFormatInfluxdb,
|
||||
config.TypeStatsdTagFormatGraphite,
|
||||
config.TypeStatsdTagFormatDatadog,
|
||||
strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatGraphite),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatDatadog),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeStatsdTagFormatTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, strings.ToLower(value), testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
config.TypeStatsdTagFormatInfluxdb,
|
||||
config.TypeStatsdTagFormatGraphite,
|
||||
config.TypeStatsdTagFormatDatadog,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeStatsdTagFormatTestStruct{
|
||||
Value: config.TypeStatsdTagFormat{
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestGet() {
|
||||
value := config.TypeStatsdTagFormat{}
|
||||
suite.Equal(config.TypeStatsdTagFormatDatadog,
|
||||
value.Get(config.TypeStatsdTagFormatDatadog))
|
||||
|
||||
suite.NoError(value.Set(config.TypeStatsdTagFormatInfluxdb))
|
||||
suite.Equal(config.TypeStatsdTagFormatInfluxdb,
|
||||
value.Get(config.TypeStatsdTagFormatDatadog))
|
||||
}
|
||||
|
||||
func TestTypeStatsdTagFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &StatsdTagFormatTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CaptureStdout(callback func()) string {
|
||||
return captureOutput(&os.Stdout, callback)
|
||||
}
|
||||
|
||||
func CaptureStderr(callback func()) string {
|
||||
return captureOutput(&os.Stderr, callback)
|
||||
}
|
||||
|
||||
func captureOutput(filefp **os.File, callback func()) string {
|
||||
oldFp := *filefp
|
||||
|
||||
defer func() {
|
||||
*filefp = oldFp
|
||||
}()
|
||||
|
||||
reader, writer, _ := os.Pipe()
|
||||
buf := &bytes.Buffer{}
|
||||
closeChan := make(chan bool)
|
||||
|
||||
go func() {
|
||||
io.Copy(buf, reader) // nolint: errcheck
|
||||
close(closeChan)
|
||||
}()
|
||||
|
||||
*filefp = writer
|
||||
|
||||
callback()
|
||||
|
||||
writer.Close()
|
||||
<-closeChan
|
||||
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package testlib
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type EventsObserverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package testlib
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type MtglibAntiReplayCacheMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MtglibAntiReplayCacheMock) SeenBefore(data []byte) bool {
|
||||
return m.Called(data).Bool(0)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MtglibNetworkMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) Dial(network, address string) (net.Conn, error) {
|
||||
args := m.Called(network, address)
|
||||
|
||||
return args.Get(0).(net.Conn), args.Error(1) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
args := m.Called(ctx, network, address)
|
||||
|
||||
return args.Get(0).(net.Conn), args.Error(1) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
|
||||
network, address string) (net.Conn, error)) *http.Client {
|
||||
return m.Called(dialFunc).Get(0).(*http.Client)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type NetConnMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (n *NetConnMock) Read(b []byte) (int, error) {
|
||||
args := n.Called(b)
|
||||
|
||||
return args.Int(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) Write(b []byte) (int, error) {
|
||||
args := n.Called(b)
|
||||
|
||||
return args.Int(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) Close() error {
|
||||
return n.Called().Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *NetConnMock) LocalAddr() net.Addr {
|
||||
return n.Called().Get(0).(net.Addr)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) RemoteAddr() net.Addr {
|
||||
return n.Called().Get(0).(net.Addr)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) SetDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *NetConnMock) SetReadDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *NetConnMock) SetWriteDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import "net/url"
|
||||
|
||||
func MakeQRCodeURL(data string) string {
|
||||
values := url.Values{}
|
||||
values.Set("qzone", "4")
|
||||
values.Set("format", "svg")
|
||||
values.Set("data", data)
|
||||
|
||||
rv := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.qrserver.com",
|
||||
Path: "/v1/create-qr-code",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return rv.String()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type MakeQRCodeURLTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *MakeQRCodeURLTestSuite) TestSomeData() {
|
||||
value := utils.MakeQRCodeURL("some data")
|
||||
|
||||
parsed, err := url.Parse(value)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal("some data", parsed.Query().Get("data"))
|
||||
suite.Equal("svg", parsed.Query().Get("format"))
|
||||
suite.Equal("api.qrserver.com", strings.TrimPrefix(parsed.Host, "www."))
|
||||
suite.Equal("v1/create-qr-code", strings.Trim(parsed.Path, "/"))
|
||||
}
|
||||
|
||||
func TestMakeQRCodeURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &MakeQRCodeURLTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/v2/network"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
net.Listener
|
||||
|
||||
bufferSize int
|
||||
}
|
||||
|
||||
func (l Listener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err // nolint: wrapcheck
|
||||
}
|
||||
|
||||
if err := network.SetClientSocketOptions(conn, l.bufferSize); err != nil {
|
||||
conn.Close()
|
||||
|
||||
return nil, fmt.Errorf("cannot set TCP options: %w", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func NewListener(bindTo string, bufferSize int) (net.Listener, error) {
|
||||
base, err := net.Listen("tcp", bindTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build a base listener: %w", err)
|
||||
}
|
||||
|
||||
return Listener{
|
||||
Listener: base,
|
||||
bufferSize: bufferSize,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
)
|
||||
|
||||
func ReadConfig(path string) (*config.Config, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read config file: %w", err)
|
||||
}
|
||||
|
||||
conf, err := config.Parse(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse config: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ReadConfigTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) GetConfigPath(filename string) string {
|
||||
return filepath.Join("testdata", filename)
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) TestReadMinimal() {
|
||||
conf, err := utils.ReadConfig(suite.GetConfigPath("minimal.toml"))
|
||||
suite.NoError(err)
|
||||
suite.NoError(conf.Validate())
|
||||
suite.Equal("0.0.0.0:80", conf.BindTo.Get(""))
|
||||
suite.Equal("7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t", conf.Secret.Base64())
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) TestReadAbsentFile() {
|
||||
_, err := utils.ReadConfig(suite.GetConfigPath("unknown.file"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) TestBrokenFile() {
|
||||
_, err := utils.ReadConfig(suite.GetConfigPath("broken.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) TestMissedBindTo() {
|
||||
_, err := utils.ReadConfig(suite.GetConfigPath("missed-bindto.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) TestMissedSecret() {
|
||||
_, err := utils.ReadConfig(suite.GetConfigPath("missed-secret.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ReadConfigTestSuite) TestEmpty() {
|
||||
_, err := utils.ReadConfig(suite.GetConfigPath("empty.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ReadConfigTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func SetLimits() error {
|
||||
rLimit := unix.Rlimit{}
|
||||
if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
return fmt.Errorf("cannot get rlimit: %w", err)
|
||||
}
|
||||
|
||||
rLimit.Cur = rLimit.Max
|
||||
|
||||
if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
return fmt.Errorf("cannot set rlimit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
func SetLimits() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func RootContext() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
for range sigChan {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
func RootContext() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
go func() {
|
||||
for range sigChan {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
11
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t"
|
||||
bind-to = "0.0.0.0:80"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
|
||||
secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t"
|
||||
+1
@@ -0,0 +1 @@
|
||||
bind-to = "0.0.0.0:80"
|
||||
@@ -0,0 +1,359 @@
|
||||
package ipblocklist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/kentik/patricia"
|
||||
"github.com/kentik/patricia/bool_tree"
|
||||
"github.com/panjf2000/ants/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
fireholIPv4DefaultCIDR = 32
|
||||
fireholIPv6DefaultCIDR = 128
|
||||
)
|
||||
|
||||
var fireholRegexpComment = regexp.MustCompile(`\s*#.*?$`)
|
||||
|
||||
// Firehol is IPBlocklist which uses lists from FireHOL:
|
||||
// https://iplists.firehol.org/
|
||||
//
|
||||
// It can use both local files and remote URLs. This is not necessary
|
||||
// that blocklists should be taken from this website, we expect only
|
||||
// compatible formats here.
|
||||
//
|
||||
// Example of the format:
|
||||
//
|
||||
// # this is a comment
|
||||
// # to ignore
|
||||
// 127.0.0.1 # you can specify an IP
|
||||
// 10.0.0.0/8 # or cidr
|
||||
type Firehol struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
logger mtglib.Logger
|
||||
|
||||
rwMutex sync.RWMutex
|
||||
|
||||
remoteURLs []string
|
||||
localFiles []string
|
||||
|
||||
httpClient *http.Client
|
||||
workerPool *ants.Pool
|
||||
|
||||
treeV4 *bool_tree.TreeV4
|
||||
treeV6 *bool_tree.TreeV6
|
||||
}
|
||||
|
||||
// Shutdown stop a background update process.
|
||||
func (f *Firehol) Shutdown() {
|
||||
f.ctxCancel()
|
||||
}
|
||||
|
||||
// Contains is given IP list can be found in FireHOL blocklists.
|
||||
func (f *Firehol) Contains(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
f.rwMutex.RLock()
|
||||
defer f.rwMutex.RUnlock()
|
||||
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return f.containsIPv4(ip4)
|
||||
}
|
||||
|
||||
return f.containsIPv6(ip.To16())
|
||||
}
|
||||
|
||||
// Run starts a background update process.
|
||||
//
|
||||
// This is a blocking method so you probably want to run it in a
|
||||
// goroutine.
|
||||
func (f *Firehol) Run(updateEach time.Duration) {
|
||||
if updateEach == 0 {
|
||||
updateEach = DefaultFireholUpdateEach
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(updateEach)
|
||||
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
if err := f.update(); err != nil {
|
||||
f.logger.WarningError("cannot update blocklist", err)
|
||||
} else {
|
||||
f.logger.Info("blocklist was updated")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := f.update(); err != nil {
|
||||
f.logger.WarningError("cannot update blocklist", err)
|
||||
} else {
|
||||
f.logger.Info("blocklist was updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Firehol) containsIPv4(addr net.IP) bool {
|
||||
ip := patricia.NewIPv4AddressFromBytes(addr, 32) // nolint: gomnd
|
||||
|
||||
if ok, _ := f.treeV4.FindDeepestTag(ip); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Firehol) containsIPv6(addr net.IP) bool {
|
||||
ip := patricia.NewIPv6Address(addr, 128) // nolint: gomnd
|
||||
|
||||
if ok, _ := f.treeV6.FindDeepestTag(ip); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Firehol) update() error { // nolint: funlen, cyclop
|
||||
ctx, cancel := context.WithCancel(f.ctx)
|
||||
defer cancel()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(f.remoteURLs) + len(f.localFiles))
|
||||
|
||||
treeMutex := &sync.Mutex{}
|
||||
v4tree := bool_tree.NewTreeV4()
|
||||
v6tree := bool_tree.NewTreeV6()
|
||||
|
||||
errorChan := make(chan error, 1)
|
||||
defer close(errorChan)
|
||||
|
||||
for _, v := range f.localFiles {
|
||||
go func(filename string) {
|
||||
defer wg.Done()
|
||||
|
||||
if err := f.updateLocalFile(ctx, filename, treeMutex, v4tree, v6tree); err != nil {
|
||||
cancel()
|
||||
f.logger.BindStr("filename", filename).WarningError("cannot update", err)
|
||||
|
||||
select {
|
||||
case errorChan <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}(v)
|
||||
}
|
||||
|
||||
for _, v := range f.remoteURLs {
|
||||
value := v
|
||||
|
||||
f.workerPool.Submit(func() { // nolint: errcheck
|
||||
defer wg.Done()
|
||||
|
||||
if err := f.updateRemoteURL(ctx, value, treeMutex, v4tree, v6tree); err != nil {
|
||||
cancel()
|
||||
f.logger.BindStr("url", value).WarningError("cannot update", err)
|
||||
|
||||
select {
|
||||
case errorChan <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errorChan:
|
||||
return fmt.Errorf("cannot update trees: %w", err)
|
||||
default:
|
||||
}
|
||||
|
||||
f.rwMutex.Lock()
|
||||
defer f.rwMutex.Unlock()
|
||||
|
||||
f.treeV4 = v4tree
|
||||
f.treeV6 = v6tree
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firehol) updateLocalFile(ctx context.Context, filename string,
|
||||
mutex sync.Locker,
|
||||
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
|
||||
filefp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open file: %w", err)
|
||||
}
|
||||
|
||||
go func(ctx context.Context, closer io.Closer) {
|
||||
<-ctx.Done()
|
||||
closer.Close()
|
||||
}(ctx, filefp)
|
||||
|
||||
defer filefp.Close()
|
||||
|
||||
return f.updateTrees(mutex, filefp, v4tree, v6tree)
|
||||
}
|
||||
|
||||
func (f *Firehol) updateRemoteURL(ctx context.Context, url string,
|
||||
mutex sync.Locker,
|
||||
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build a request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := f.httpClient.Do(req) // nolint: bodyclose
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot request a remote URL %s: %w", url, err)
|
||||
}
|
||||
|
||||
go func(ctx context.Context, closer io.Closer) {
|
||||
<-ctx.Done()
|
||||
closer.Close()
|
||||
}(ctx, resp.Body)
|
||||
|
||||
defer func(rc io.ReadCloser) {
|
||||
io.Copy(io.Discard, rc) // nolint: errcheck
|
||||
rc.Close()
|
||||
}(resp.Body)
|
||||
|
||||
return f.updateTrees(mutex, resp.Body, v4tree, v6tree)
|
||||
}
|
||||
|
||||
func (f *Firehol) updateTrees(mutex sync.Locker,
|
||||
reader io.Reader,
|
||||
v4tree *bool_tree.TreeV4,
|
||||
v6tree *bool_tree.TreeV6) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
text = fireholRegexpComment.ReplaceAllLiteralString(text, "")
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ip, cidr, err := f.updateParseLine(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse a line: %w", err)
|
||||
}
|
||||
|
||||
f.updateAddToTrees(ip, cidr, mutex, v4tree, v6tree)
|
||||
}
|
||||
|
||||
if scanner.Err() != nil {
|
||||
return fmt.Errorf("cannot parse a response: %w", scanner.Err())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firehol) updateParseLine(text string) (net.IP, uint, error) {
|
||||
_, ipnet, err := net.ParseCIDR(text)
|
||||
if err != nil {
|
||||
ipaddr := net.ParseIP(text)
|
||||
if ipaddr == nil {
|
||||
return nil, 0, fmt.Errorf("incorrect ip address %s", text)
|
||||
}
|
||||
|
||||
ip4 := ipaddr.To4()
|
||||
if ip4 != nil {
|
||||
return ip4, fireholIPv4DefaultCIDR, nil
|
||||
}
|
||||
|
||||
return ipaddr.To16(), fireholIPv6DefaultCIDR, nil
|
||||
}
|
||||
|
||||
ones, _ := ipnet.Mask.Size()
|
||||
|
||||
return ipnet.IP, uint(ones), nil
|
||||
}
|
||||
|
||||
func (f *Firehol) updateAddToTrees(ip net.IP, cidr uint,
|
||||
mutex sync.Locker,
|
||||
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if ip.To4() != nil {
|
||||
v4tree.Set(patricia.NewIPv4AddressFromBytes(ip, cidr), true)
|
||||
} else {
|
||||
v6tree.Set(patricia.NewIPv6Address(ip, cidr), true)
|
||||
}
|
||||
}
|
||||
|
||||
// NewFirehol creates a new instance of FireHOL IP blocklist.
|
||||
//
|
||||
// This method does not start an update process so please execute Run
|
||||
// when it is necessary.
|
||||
func NewFirehol(logger mtglib.Logger, network mtglib.Network,
|
||||
downloadConcurrency uint,
|
||||
remoteURLs []string,
|
||||
localFiles []string) (*Firehol, error) {
|
||||
for _, v := range remoteURLs {
|
||||
parsed, err := url.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("incorrect url %s: %w", v, err)
|
||||
}
|
||||
|
||||
switch parsed.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported url %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range localFiles {
|
||||
if stat, err := os.Stat(v); os.IsNotExist(err) || stat.IsDir() || stat.Mode().Perm()&0o400 == 0 {
|
||||
return nil, fmt.Errorf("%s is not a readable file", v)
|
||||
}
|
||||
}
|
||||
|
||||
if downloadConcurrency == 0 {
|
||||
downloadConcurrency = DefaultFireholDownloadConcurrency
|
||||
}
|
||||
|
||||
workerPool, _ := ants.NewPool(int(downloadConcurrency))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Firehol{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
logger: logger.Named("firehol"),
|
||||
httpClient: network.MakeHTTPClient(nil),
|
||||
treeV4: bool_tree.NewTreeV4(),
|
||||
treeV6: bool_tree.NewTreeV6(),
|
||||
workerPool: workerPool,
|
||||
remoteURLs: remoteURLs,
|
||||
localFiles: localFiles,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package ipblocklist_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/9seconds/mtg/v2/network"
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type FireholTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
networkMock *testlib.MtglibNetworkMock
|
||||
httpServer *httptest.Server
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) SetupSuite() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
filefp, err := os.Open(filepath.Join("testdata", "remote_ipset.ipset"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer filefp.Close()
|
||||
|
||||
io.Copy(w, filefp) // nolint: errcheck
|
||||
})
|
||||
|
||||
suite.httpServer = httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) SetupTest() {
|
||||
httpClient := &http.Client{}
|
||||
suite.networkMock = &testlib.MtglibNetworkMock{}
|
||||
|
||||
httpmock.ActivateNonDefault(httpClient)
|
||||
|
||||
suite.networkMock.
|
||||
On("MakeHTTPClient", mock.Anything).
|
||||
Maybe().
|
||||
Return(httpClient)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TearDownTest() {
|
||||
suite.networkMock.AssertExpectations(suite.T())
|
||||
httpmock.DeactivateAndReset()
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TearDownSuite() {
|
||||
suite.httpServer.Close()
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestLocalFail() {
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
suite.networkMock, 2,
|
||||
nil, []string{filepath.Join("testdata", "broken_ipset.ipset")})
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.False(blocklist.Contains(net.ParseIP("10.0.0.10")))
|
||||
suite.False(blocklist.Contains(net.ParseIP("127.0.0.1")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestLocalOk() {
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
suite.networkMock, 2,
|
||||
nil, []string{filepath.Join("testdata", "good_ipset.ipset")})
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.True(blocklist.Contains(net.ParseIP("10.0.0.10")))
|
||||
suite.False(blocklist.Contains(net.ParseIP("127.0.0.1")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestRemoteFail() {
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
suite.networkMock, 2,
|
||||
[]string{"https://google.com"}, nil)
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.False(blocklist.Contains(net.ParseIP("10.2.2.2")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestMixed() {
|
||||
dialer, _ := network.NewDefaultDialer(0, 0)
|
||||
ntw, _ := network.NewNetwork(dialer, "mtg", "1.1.1.1", 0)
|
||||
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
ntw, 2,
|
||||
[]string{
|
||||
suite.httpServer.URL,
|
||||
}, []string{
|
||||
filepath.Join("testdata", "good_ipset.ipset"),
|
||||
})
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.True(blocklist.Contains(net.ParseIP("10.2.2.2")))
|
||||
suite.True(blocklist.Contains(net.ParseIP("10.1.0.100")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestFirehol(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &FireholTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Package ipblocklist contains default implementation of the
|
||||
// IPBlocklist for mtg.
|
||||
//
|
||||
// Please check documentation for mtglib.IPBlocklist interface to get an
|
||||
// idea of this abstraction.
|
||||
package ipblocklist
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
// DefaultFireholDownloadConcurrency defines a default max number of
|
||||
// concurrent downloads of ip blocklists for Firehol.
|
||||
DefaultFireholDownloadConcurrency = 1
|
||||
|
||||
// DefaultFireholUpdateEach defines a default time period when
|
||||
// Firehol requests updates of the blocklists.
|
||||
DefaultFireholUpdateEach = 6 * time.Hour
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package ipblocklist
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) Contains(ip net.IP) bool { return false }
|
||||
|
||||
// NewNoop returns a dummy ipblocklist which allows all incoming
|
||||
// connections.
|
||||
func NewNoop() mtglib.IPBlocklist {
|
||||
return noop{}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ipblocklist_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestOp() {
|
||||
suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10")))
|
||||
suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10")))
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopTestSuite{})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user