mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 16:04:02 +03:00
REPOSITORY / ScuroNeko/mtg
Compare commits
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6ba6d8c6 | ||
|
|
be398f9c79 | ||
|
|
4c029fc683 | ||
|
|
3066672353 | ||
|
|
ae88c0cab0 | ||
|
|
816b7f72b6 | ||
|
|
1f7584c108 | ||
|
|
2fb025bbfd | ||
|
|
c7ab53134a | ||
|
|
f83ee17361 | ||
|
|
36546cec2f | ||
|
|
074c4017f5 | ||
|
|
8e87405d3e | ||
|
|
82679ec20f | ||
|
|
308e372a5d | ||
|
|
836a481026 | ||
|
|
0a5a45b32d | ||
|
|
852ca713c8 | ||
|
|
301bde88ac | ||
|
|
25bca76da5 | ||
|
|
76109f6204 | ||
|
|
6db6415798 | ||
|
|
0faf482e52 | ||
|
|
3e4faf6ba6 | ||
|
|
f7f6a7637c | ||
|
|
130b02013b | ||
|
|
1b4c777ed3 | ||
|
|
60b4b5ad8f | ||
|
|
a4930a596a | ||
|
|
fb3e921bc6 | ||
|
|
afbf4f6f74 | ||
|
|
592b7c5b87 | ||
|
|
c170887499 | ||
|
|
ad76304b43 | ||
|
|
5e38819506 | ||
|
|
f06fd5ca77 | ||
|
|
ad88afeecd | ||
|
|
c74c0c92c6 | ||
|
|
e3dc03b4db | ||
|
|
ecba88d2e3 | ||
|
|
37de052feb | ||
|
|
2041f3154b | ||
|
|
b1728c3474 | ||
|
|
b72af2b953 | ||
|
|
75c317f35e | ||
|
|
795dabb80b | ||
|
|
01b0166995 | ||
|
|
e9a147c836 | ||
|
|
de66835657 | ||
|
|
75f11965d8 | ||
|
|
e68d0c7da5 | ||
|
|
051180a170 | ||
|
|
b4e0143cb7 | ||
|
|
269852a4f1 | ||
|
|
bd8a7ed8c9 | ||
|
|
7e3e4a611d | ||
|
|
db2be4001b | ||
|
|
05c99aee4a | ||
|
|
53dde2aafa | ||
|
|
36dad5a2f6 | ||
|
|
6a19ded78e | ||
|
|
008e17cdff | ||
|
|
5ad64390d6 | ||
|
|
f9ad93dd06 | ||
|
|
77edea0e18 | ||
|
|
79f921fde9 | ||
|
|
1cdaaa16b0 | ||
|
|
5852f48ca5 | ||
|
|
0e80222990 | ||
|
|
c07e3d573d | ||
|
|
f192a718f4 | ||
|
|
0c4523d2c8 | ||
|
|
b0063a4a25 | ||
|
|
6dc494e7d0 | ||
|
|
534d5b755e | ||
|
|
aa7e488a3a | ||
|
|
eba3673e27 | ||
|
|
a27facaa16 | ||
|
|
78a9ff18a5 | ||
|
|
c0899d0743 | ||
|
|
12563219e6 | ||
|
|
59b5ff4080 | ||
|
|
d7e554540d | ||
|
|
1575b82688 | ||
|
|
5df1f594cc | ||
|
|
02ad052c49 | ||
|
|
2b8c7825ca | ||
|
|
75357b3e3c | ||
|
|
d8b14dc765 | ||
|
|
e0850869ba | ||
|
|
9375552180 | ||
|
|
cbe5b8c94e | ||
|
|
63b425f245 | ||
|
|
b53ead4372 | ||
|
|
2cdd66c722 | ||
|
|
2ae0101d18 | ||
|
|
6da9c2e58d | ||
|
|
6d89f14c9b | ||
|
|
442e2da330 | ||
|
|
a9b3560b25 | ||
|
|
880dd04728 | ||
|
|
8ea1aa3f5e | ||
|
|
4e5a83cfe9 | ||
|
|
5282ca26f3 | ||
|
|
2077db1f1e | ||
|
|
1a9bc80091 | ||
|
|
30170b9413 | ||
|
|
4687a7c899 | ||
|
|
d467fba674 | ||
|
|
6e447b1d59 | ||
|
|
b15a8ec4a5 | ||
|
|
6bef4df091 | ||
|
|
0ce0c668b9 | ||
|
|
01e201365b | ||
|
|
0b52367a82 | ||
|
|
25c842daf1 | ||
|
|
4c543aaea2 | ||
|
|
fee133a62f | ||
|
|
7d38fec74e | ||
|
|
15bb5be6c4 | ||
|
|
ad8c09a2a3 | ||
|
|
2f626e2138 | ||
|
|
93bed24a0b | ||
|
|
219235e181 | ||
|
|
79f54a4e67 | ||
|
|
4b78e83be7 | ||
|
|
ad30eca406 | ||
|
|
ecf947b86c | ||
|
|
5be581154e | ||
|
|
bc3b517e8b | ||
|
|
3f8f96b91f | ||
|
|
ef55fbba15 | ||
|
|
e7416bc04d | ||
|
|
9a6264a89f | ||
|
|
33e0509c5a | ||
|
|
ffad717829 | ||
|
|
7b1f86b75d | ||
|
|
a5e59d9ef7 | ||
|
|
16c06f247c | ||
|
|
0ddaabb136 | ||
|
|
558fec60de | ||
|
|
cc101c9a47 | ||
|
|
e6fa69d288 | ||
|
|
c14a2329c5 | ||
|
|
4c75066ef8 | ||
|
|
ce8163d1b7 | ||
|
|
ca77157fd5 | ||
|
|
66f4d967e7 | ||
|
|
d19cfb1df4 | ||
|
|
3540408adf | ||
|
|
7917434a37 | ||
|
|
853395106b | ||
|
|
d1b0d1f133 | ||
|
|
73bd7287f0 | ||
|
|
31b8ab4482 | ||
|
|
bae5407372 | ||
|
|
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 |
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
fixes:
|
||||||
|
- "github.com/9seconds/mtg/v2/::"
|
||||||
+7
-1
@@ -1,3 +1,9 @@
|
|||||||
version.go
|
|
||||||
vendor/
|
vendor/
|
||||||
tags
|
tags
|
||||||
|
.github/
|
||||||
|
.bin/
|
||||||
|
*.md
|
||||||
|
mtg
|
||||||
|
.golangci.toml
|
||||||
|
.gitignore
|
||||||
|
run.sh
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
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: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: mise tasks run covtest
|
||||||
|
|
||||||
|
- name: Collect coverage
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
files: ./coverage.txt
|
||||||
|
|
||||||
|
fuzz:
|
||||||
|
name: Fuzzing
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Run fuzzing
|
||||||
|
run: mise tasks run 'test:fuzz:*'
|
||||||
|
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: mise tasks run lint
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Get Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
nineseconds/mtg
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }}
|
||||||
|
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Setup BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Setup cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- 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.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.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
|
||||||
+2
-4
@@ -6,7 +6,5 @@
|
|||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
mtg
|
mtg
|
||||||
vendor/
|
coverage.txt
|
||||||
version.go
|
dist/
|
||||||
ccbuilds/
|
|
||||||
.bin/
|
|
||||||
|
|||||||
+17
-9
@@ -1,13 +1,21 @@
|
|||||||
|
# https://golangci-lint.run/docs/configuration/file/
|
||||||
|
|
||||||
|
version = '2'
|
||||||
|
|
||||||
[run]
|
[run]
|
||||||
concurrency = 4
|
concurrency = 4
|
||||||
deadline = "2m"
|
|
||||||
tests = true
|
tests = true
|
||||||
skip-dirs = ["vendor"]
|
|
||||||
skip-files = ["version.go"]
|
|
||||||
|
|
||||||
[output]
|
# [linters]
|
||||||
format = "colored-line-number"
|
# enable-all = true
|
||||||
|
# disable = [
|
||||||
[linters]
|
# "containedctx",
|
||||||
enable-all = true
|
# "exhaustivestruct",
|
||||||
disable = ["gochecknoglobals", "gas", "gomnd", "goerr113"]
|
# "exhaustruct",
|
||||||
|
# "gas",
|
||||||
|
# "gochecknoglobals",
|
||||||
|
# "goerr113",
|
||||||
|
# "ireturn",
|
||||||
|
# "thelper",
|
||||||
|
# "varnamelen",
|
||||||
|
# ]
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
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 }}'
|
||||||
|
formats:
|
||||||
|
- tar.gz
|
||||||
|
wrap_in_directory: true
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- SECURITY.md
|
||||||
|
|
||||||
|
gomod:
|
||||||
|
proxy: true
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
version_template: '{{ .Version }}'
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
|
||||||
|
|
||||||
|
source:
|
||||||
|
enabled: true
|
||||||
|
name_template: '{{ .ProjectName }}-sources'
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
[tools]
|
||||||
|
go = "latest"
|
||||||
|
"go:golang.org/x/tools/gopls" = "latest"
|
||||||
|
"go:mvdan.cc/gofumpt" = "latest"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
fuzzflags = "-fuzztime=120s"
|
||||||
|
|
||||||
|
[tasks.build]
|
||||||
|
description = "Build binary"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum"]
|
||||||
|
outputs = ["mtg"]
|
||||||
|
run = "go build"
|
||||||
|
|
||||||
|
[tasks.update]
|
||||||
|
description = "Update dependencies"
|
||||||
|
run = [
|
||||||
|
"go get -u",
|
||||||
|
"go mod tidy -go=1.25"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.lint]
|
||||||
|
description = "Run linter"
|
||||||
|
tools.golangci-lint = "latest"
|
||||||
|
run = "golangci-lint run"
|
||||||
|
|
||||||
|
[tasks.test]
|
||||||
|
description = "Run tests"
|
||||||
|
run = "go test -v ./..."
|
||||||
|
|
||||||
|
[tasks.covtest]
|
||||||
|
description = "Run tests with code coverage"
|
||||||
|
run = "go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./..."
|
||||||
|
|
||||||
|
[tasks.test-all]
|
||||||
|
description = "Run all tests"
|
||||||
|
depends = [
|
||||||
|
"test",
|
||||||
|
"test:fuzz:*"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."test:fuzz:client-hello"]
|
||||||
|
description = "Run fuzzy test for ClientHello"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/faketls"
|
||||||
|
|
||||||
|
[tasks."test:fuzz:client-handshake"]
|
||||||
|
description = "Run fuzzy test for ClientHandshake"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHandshake ./mtglib/internal/obfuscated2"
|
||||||
|
|
||||||
|
[tasks."test:fuzz:server-generate-handshake-frame"]
|
||||||
|
description = "Run fuzzy test for ServerGenerateHandshakeFrame"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerGenerateHandshakeFrame ./mtglib/internal/obfuscated2"
|
||||||
|
|
||||||
|
[tasks."test:fuzz:server-receive"]
|
||||||
|
description = "Run fuzzy test for ServerReceive"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerReceive ./mtglib/internal/obfuscated2"
|
||||||
|
|
||||||
|
[tasks."test:fuzz:server-send"]
|
||||||
|
description = "Run fuzzy test for ServerSend"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerSend ./mtglib/internal/obfuscated2"
|
||||||
|
|
||||||
|
[tasks.static]
|
||||||
|
description = "Build static binary"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum"]
|
||||||
|
outputs = ["mtg"]
|
||||||
|
run = """
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)"
|
||||||
|
go build \
|
||||||
|
-trimpath \
|
||||||
|
-mod=readonly \
|
||||||
|
-ldflags="-extldflags '-static' -s -w -X 'main.version=$version'" \
|
||||||
|
-a \
|
||||||
|
-tags netgo
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.release]
|
||||||
|
description = "Create release tarballs"
|
||||||
|
tools.goreleaser = "latest"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum", ".goreleaser.yml"]
|
||||||
|
run = [
|
||||||
|
"goreleaser --snapshot --clean",
|
||||||
|
"find dist -depth 1 -type d | xargs -r rm -r",
|
||||||
|
"rm ./dist/config.yaml"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.image]
|
||||||
|
description = "Build docker image"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum", "Dockerfile"]
|
||||||
|
run = "docker buildx build --pull -t mtg ."
|
||||||
|
|
||||||
|
[tasks.docs]
|
||||||
|
description = "Run doc server"
|
||||||
|
tools."go:golang.org/x/pkgsite/cmd/pkgsite" = "latest"
|
||||||
|
run = "pkgsite -http 0.0.0.0:10000"
|
||||||
|
|
||||||
|
[tasks.fmt]
|
||||||
|
description = "Reformat source code"
|
||||||
|
sources = ["**/*.go"]
|
||||||
|
run = "gofumpt -w --extra ."
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
dist: trusty
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.15.x
|
|
||||||
- 1.14.x
|
|
||||||
- 1.13.x
|
|
||||||
- master
|
|
||||||
|
|
||||||
before_script: make prepare
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make all
|
|
||||||
- make lint
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- go: master
|
|
||||||
+16
-13
@@ -1,23 +1,27 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# BUILD STAGE
|
# BUILD STAGE
|
||||||
|
|
||||||
FROM golang:1.15-alpine AS build
|
FROM golang:1.26-alpine AS build
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apk --no-cache --update add \
|
&& apk --no-cache --update add \
|
||||||
bash \
|
bash \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
git
|
||||||
git \
|
|
||||||
make \
|
|
||||||
upx
|
|
||||||
|
|
||||||
COPY . /go/src/github.com/9seconds/mtg/
|
COPY . /app
|
||||||
WORKDIR /go/src/github.com/9seconds/mtg
|
WORKDIR /app
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& make -j 4 static \
|
&& version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)" \
|
||||||
&& upx --ultra-brute -qq ./mtg
|
&& go build \
|
||||||
|
-trimpath \
|
||||||
|
-mod=readonly \
|
||||||
|
-ldflags="-extldflags '-static' -s -w -X 'main.version=$version'" \
|
||||||
|
-a \
|
||||||
|
-tags netgo
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -26,9 +30,8 @@ RUN set -x \
|
|||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
ENTRYPOINT ["/mtg"]
|
ENTRYPOINT ["/mtg"]
|
||||||
ENV MTG_BIND=0.0.0.0:3128 \
|
CMD ["run", "/config.toml"]
|
||||||
MTG_STATS_BIND=0.0.0.0:3129
|
|
||||||
EXPOSE 3128 3129
|
|
||||||
|
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
COPY --from=build /go/src/github.com/9seconds/mtg/mtg /mtg
|
COPY --from=build /app/mtg /mtg
|
||||||
|
COPY --from=build /app/example.config.toml /config.toml
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
IMAGE_NAME := mtg
|
|
||||||
APP_NAME := $(IMAGE_NAME)
|
|
||||||
|
|
||||||
CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}")
|
|
||||||
|
|
||||||
GOLANGCI_LINT_VERSION := v1.31.0
|
|
||||||
|
|
||||||
VERSION_GO := $(shell go version)
|
|
||||||
VERSION_DATE := $(shell date -Ru)
|
|
||||||
VERSION_TAG := $(shell git describe --tags --always)
|
|
||||||
COMMON_BUILD_FLAGS := -ldflags="-s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
|
|
||||||
|
|
||||||
MOD_ON := env GO111MODULE=on
|
|
||||||
MOD_OFF := env GO111MODULE=auto
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
$(APP_NAME):
|
|
||||||
@$(MOD_ON) go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
|
||||||
|
|
||||||
static-$(APP_NAME):
|
|
||||||
@$(MOD_ON) 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)-%: ccbuilds
|
|
||||||
@$(MOD_ON) env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
|
|
||||||
go build \
|
|
||||||
$(COMMON_BUILD_FLAGS) \
|
|
||||||
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
|
|
||||||
|
|
||||||
ccbuilds:
|
|
||||||
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
|
|
||||||
|
|
||||||
vendor: go.mod go.sum
|
|
||||||
@$(MOD_ON) go mod vendor
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: $(APP_NAME)
|
|
||||||
|
|
||||||
.PHONY: static
|
|
||||||
static: static-$(APP_NAME)
|
|
||||||
|
|
||||||
.PHONY: crosscompile
|
|
||||||
crosscompile: $(CC_BINARIES)
|
|
||||||
|
|
||||||
.PHONY: crosscompile-dir
|
|
||||||
crosscompile-dir:
|
|
||||||
@rm -rf "$(CC_DIR)" && mkdir -p "$(CC_DIR)"
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: vendor
|
|
||||||
@$(MOD_OFF) "$(ROOT_DIR)/.bin/golangci-lint" run
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean:
|
|
||||||
@git clean -xfd && \
|
|
||||||
git reset --hard >/dev/null && \
|
|
||||||
git submodule foreach --recursive sh -c 'git clean -xfd && git reset --hard' >/dev/null
|
|
||||||
|
|
||||||
.PHONY: docker
|
|
||||||
docker:
|
|
||||||
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
|
|
||||||
|
|
||||||
.PHONY: prepare
|
|
||||||
prepare: install-lint
|
|
||||||
|
|
||||||
.PHONY: install-lint
|
|
||||||
install-lint:
|
|
||||||
@mkdir -p ./bin || true && \
|
|
||||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
|
|
||||||
| $(MOD_OFF) bash -s -- -b "$(ROOT_DIR)/.bin" $(GOLANGCI_LINT_VERSION)
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
# mtg
|
# mtg
|
||||||
|
|
||||||
Bullshit-free MTPROTO proxy for Telegram
|
Highly-opinionated (ex-bullshit-free) MTPROTO proxy for
|
||||||
|
[Telegram](https://telegram.org/).
|
||||||
|
|
||||||
[](https://travis-ci.org/9seconds/mtg)
|
[](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
|
||||||
[](https://goreportcard.com/report/github.com/9seconds/mtg)
|
[](https://codecov.io/gh/9seconds/mtg)
|
||||||
[](https://hub.docker.com/r/nineseconds/mtg/)
|
[](https://pkg.go.dev/github.com/9seconds/mtg/v2)
|
||||||
|
|
||||||
**Please see a guide on upgrading to 1.0 at the end of this README.**
|
**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
|
There are several available proxies for Telegram MTPROTO available. Here
|
||||||
are the most notable:
|
are the most notable:
|
||||||
@@ -17,268 +19,388 @@ are the most notable:
|
|||||||
* [Python](https://github.com/alexbers/mtprotoproxy)
|
* [Python](https://github.com/alexbers/mtprotoproxy)
|
||||||
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
||||||
|
|
||||||
Almost all of them follow the way how official proxy was built. This
|
You can use any of these. They work great and all implementations have
|
||||||
includes support of multiple secrets, support of promoted channels, etc.
|
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 few resources as possible but not by losing
|
|
||||||
maintainability.
|
|
||||||
* **Easily deployable**
|
* **Easily deployable**
|
||||||
|
|
||||||
I strongly believe that Telegram proxies should follow the way of
|
I strongly believe that Telegram proxies should follow the way of
|
||||||
ShadowSocks: promoted channels is a strange way of doing business
|
[ShadowSocks](https://shadowsocks.org): promoted channels is a strange
|
||||||
I suppose. I think the only viable way is to have a proxy with
|
way of doing business I suppose. I think the only viable way is to
|
||||||
minimum configuration which should work everywhere.
|
have a proxy that can be restored anywhere easily.
|
||||||
|
|
||||||
* **A single secret**
|
* **A single secret**
|
||||||
I think that multiple secrets solve no problems and just complexify
|
|
||||||
|
I think that multiple secrets solve no problems and just complex
|
||||||
software. I also believe that in the case of throwout proxies, this
|
software. I also believe that in the case of throwout proxies, this
|
||||||
feature is a useless luxury.
|
the feature is a useless luxury.
|
||||||
* **Minimum docker image size**
|
|
||||||
Official image is less than 3.5 megabytes. Literally.
|
* **No adtag support**
|
||||||
|
|
||||||
|
Please read [Version 2](#version-2) chapter.
|
||||||
|
|
||||||
* **No management WebUI**
|
* **No management WebUI**
|
||||||
|
|
||||||
This is an implementation of a simple lightweight proxy. I won't do that.
|
This is an implementation of a simple lightweight proxy. I won't do that.
|
||||||
|
|
||||||
This proxy supports 2 modes of work: direct connection to Telegram and
|
* **Proxy chaining**
|
||||||
promoted channel mode. If you do not need promoted channels, I would
|
|
||||||
recommend you to go with direct mode: this way is more robust.
|
|
||||||
|
|
||||||
To run a proxy in direct mode, all you need to do is just provide a
|
mtg has the support of [SOCKS5](https://en.wikipedia.org/wiki/SOCKS)
|
||||||
secret. If you do not provide ADTag as a second parameter, promoted
|
proxies. So, in theory, you can run this proxy as a frontend
|
||||||
channels mode won't be activated.
|
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.
|
||||||
|
|
||||||
To get promoted channel, please contact
|
* **Native blocklist support**
|
||||||
[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
|
|
||||||
second parameter.
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
# Source code organization
|
* **Can be used as a library**
|
||||||
|
|
||||||
There are 2 main branches:
|
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.
|
||||||
|
|
||||||
1. `master` branch contains potentially unstable features
|
### Version 2
|
||||||
2. `stable` branch contains stable version. Usually you want to use this branch.
|
|
||||||
|
|
||||||
# How to build
|
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
|
```console
|
||||||
make
|
docker pull nineseconds/mtg:2
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to build for another platform:
|
For version 1:
|
||||||
|
|
||||||
```console
|
```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
|
||||||
|
go install github.com/9seconds/mtg/v2@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build from sources
|
||||||
|
|
||||||
|
```console
|
||||||
|
git clone https://github.com/9seconds/mtg.git
|
||||||
|
cd mtg
|
||||||
|
make static
|
||||||
|
```
|
||||||
|
|
||||||
|
or for the docker image:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
make docker
|
make docker
|
||||||
```
|
```
|
||||||
|
|
||||||
# Docker image
|
### Generate secret
|
||||||
|
|
||||||
Docker follows the same policy as the source code organization:
|
If you already have a secret in Base64 format or that, which starts with `ee`,
|
||||||
|
you can skip this chapter. Otherwise:
|
||||||
- `latest` mirrors the master branch
|
|
||||||
- `stable` mirrors the stable branch
|
|
||||||
- tags are for tagged releases
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
docker pull nineseconds/mtg:latest
|
$ mtg generate-secret google.com
|
||||||
|
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
```console
|
```console
|
||||||
docker pull nineseconds/mtg:stable
|
$ mtg generate-secret --hex google.com
|
||||||
|
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
equivalent commands with docker:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
docker pull nineseconds/mtg:0.10
|
$ docker run --rm nineseconds/mtg:2 generate-secret google.com
|
||||||
|
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||||
|
|
||||||
|
$ docker run --rm nineseconds/mtg:2 generate-secret --hex google.com
|
||||||
|
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||||
```
|
```
|
||||||
|
|
||||||
# Ansible role
|
This secret is a keystone for a proxy and your password for a client.
|
||||||
|
You need to keep it secured.
|
||||||
|
|
||||||
You can find unofficial Ansible role for mtg here: https://github.com/rlex/ansible-role-mtg
|
We recommend choosing a hostname wisely. Here we have a _google.com_
|
||||||
Also, there is another project on Ansible Galaxy: https://galaxy.ansible.com/ivansible/lin_mtproxy
|
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.
|
||||||
|
|
||||||
# Configuration
|
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.
|
||||||
|
|
||||||
To run this tool you need to configure as less as possible. Telegram
|
|
||||||
clients support 3 different secret types:
|
|
||||||
|
|
||||||
* Simple - basically, it is just a flow of frames ciphered by AES-CTR stream
|
### Simple run mode
|
||||||
cipher.
|
|
||||||
* Secured - the same stream as simple but with some random noise to prevent
|
|
||||||
statistical analysis of traffic flow.
|
|
||||||
* FakeTLS - this mode envelops telegram stream in TLS so it looks (in theory)
|
|
||||||
the same as any TLS1.3 traffic from DPI point of view.
|
|
||||||
|
|
||||||
If you do not have preferences, go with FakeTLS or at least secured.
|
mtg supports 2 modes: simple and normal. Simple mode allows starting
|
||||||
Simple mode is a little bit naive and traffic flow can be easily
|
proxy with a small subset of configuration options you usually want to
|
||||||
identified as Telegram one.
|
modify. This is quite good for oneliners that you can copy-paste and do
|
||||||
|
not bother about external files whatsoever.
|
||||||
|
|
||||||
Unlike the rest of implementation, mtg is quite strict about the
|
Let's take a look:
|
||||||
execution mode: if you run a proxy instance with FakeTLS secret, you
|
|
||||||
can't connect to it with simple or secured clients. You can't connect
|
|
||||||
to the proxy with secured secret with FakeTLS key. It forces one mode
|
|
||||||
of working. So, unfortunately, there is no way how to connect to the
|
|
||||||
deployed proxy with another secret (if you know how to construct and
|
|
||||||
convert them). But at the same time, old clients can't connect so they
|
|
||||||
won't expose the type of the service.
|
|
||||||
|
|
||||||
First, you need to generate a secret:
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg generate-secret simple
|
Usage: mtg simple-run <bind-to> <secret>
|
||||||
52a493bdfb90eea55739eabff2d92a14
|
|
||||||
|
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=1.1.1.1 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.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
So, if you want to startup a proxy with CLI only, you can do something like
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg generate-secret secured
|
$ mtg simple-run -n 1.1.1.1 -t 30s -a 512kib 127.0.0.1:3128 7hBO-dCS4EBzenlKbdLFxyNnb29nbGUuY29t
|
||||||
ddf05fb7acb549be047a7c585116581418
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Prepare a configuration file
|
||||||
|
|
||||||
|
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
|
```console
|
||||||
$ mtg generate-secret -c google.com tls
|
$ cat /etc/systemd/system/mtg.service
|
||||||
ee852380f362a09343efb4690c4e17862e676f6f676c652e636f6d
|
[Unit]
|
||||||
|
Description=mtg - MTProto proxy server
|
||||||
|
Documentation=https://github.com/9seconds/mtg
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
DynamicUser=true
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
$ sudo systemctl daemon-reload
|
||||||
|
$ sudo systemctl enable mtg
|
||||||
|
$ sudo systemctl start mtg
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer docker:
|
or you can run a docker image
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker run --rm nineseconds/mtg generate-secret tls -c bing.com
|
docker run -d -v $PWD/config.toml:/config.toml -p 443:3128 --name mtg-proxy --restart=unless-stopped nineseconds/mtg:2
|
||||||
eedf71035a8ed48a623d8e83e66aec4d0562696e672e636f6d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Antireplay cache
|
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.
|
||||||
|
|
||||||
To prevent replay attacks, we have internal storage of first frames
|
### Access a proxy
|
||||||
messages for connected clients. These frames are generated randomly
|
|
||||||
by design and we have the negligible possibility of duplication
|
|
||||||
(probability is 1/(2^64)) but it could be quite effective to prevent
|
|
||||||
replays.
|
|
||||||
|
|
||||||
It is possible to disable this cache. To do that, please explicitly set
|
Now you can generate some useful links:
|
||||||
its size to 0.
|
|
||||||
|
|
||||||
|
|
||||||
## FakeTLS
|
|
||||||
|
|
||||||
If you run this a proxy in faketls mode, this proxy will try to hide
|
|
||||||
itself cloaking a host provided as a part of the generated secret. It
|
|
||||||
means that if you cloak google.com then you can curl this proxy and
|
|
||||||
you'll get a google.com response back.
|
|
||||||
|
|
||||||
mtg proxies L3 traffic. In other words, only TCP, without interfering in
|
|
||||||
TLS, HTTP or any other high-level protocol.
|
|
||||||
|
|
||||||
|
|
||||||
## Environment variables
|
|
||||||
|
|
||||||
It is possible to configure this tool using environment variables. You
|
|
||||||
can configure any flag but not secret or adtag. Here is the list of
|
|
||||||
supported environment variables:
|
|
||||||
|
|
||||||
| Environment variable | Corresponding flags | Default value | Description |
|
|
||||||
|-------------------------------|------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `MTG_DEBUG` | `-d`, `--debug` | `false` | Run in debug mode. Usually, you need to run in this mode only if you develop this tool or its maintainer is asking you to provide logs with such verbosity. |
|
|
||||||
| `MTG_VERBOSE` | `-v`, `--verbose` | `false` | Run in verbose mode. This is way less chatty than debug mode. |
|
|
||||||
| `MTG_BIND` | `-b`, `--bind` | `0.0.0.0:3128` | Which host/port pair should we bind to (listen on). |
|
|
||||||
| `MTG_IPV4` | `-4`, `--public-ipv4` | [Autodetect](https://ifconfig.co) | IPv4 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv4 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. |
|
|
||||||
| `MTG_IPV6` | `-6`, `--public-ipv6` | [Autodetect](https://ifconfig.co) | IPv6 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv6 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. |
|
|
||||||
| `MTG_STATS_BIND` | `-t`, `--stats-bind` | `127.0.0.1:3129` | Which hist:port should we bind the internal statistics HTTP server (Prometheus). |
|
|
||||||
| `MTG_STATS_NAMESPACE` | `--stats-namespace` | `mtg` | Which namespace should be used for prometheus metrics. |
|
|
||||||
| `MTG_STATSD_ADDR` | `--statsd-addr` | | IP:host addresses of statsd service. No defaults, by defaults we do not send anything there. |
|
|
||||||
| `MTG_STATSD_PORT` | `--statsd-port` | `8125` | Which port should we use to work with statsd. |
|
|
||||||
| `MTG_STATSD_PREFIX` | `--statsd-prefix` | `mtg` | Which bucket prefix we should use. For example, if you set `mtg`, then metric `traffic.ingress` would be send as `mtg.traffic.ingress`. |
|
|
||||||
| `MTG_STATSD_TAGS_FORMAT` | `--statsd-tags-format` | | Which tags format we should use. By default, we are using default vanilla statsd tags format but if you want to send directly to InfluxDB or Datadog, please specify it there. Possible options are `influxdb` and `datadog`. |
|
|
||||||
| `MTG_STATSD_TAGS` | `--statsd-tags` | | Which tags should we send to statsd with our metrics. Please specify them as `key=value` pairs. |
|
|
||||||
| `MTG_BUFFER_WRITE` | `-w`, `--write-buffer` | `32KB` | The size of TCP write buffer in bytes. Write buffer is the buffer for messages which are going from client to Telegram. |
|
|
||||||
| `MTG_BUFFER_READ` | `-r`, `--read-buffer` | `32KB` | The size of TCP read buffer in bytes. Read buffer is the buffer for messages from Telegram to client. |
|
|
||||||
| `MTG_ANTIREPLAY_MAXSIZE` | `--anti-replay-max-size` | `128MB` | Max size of antireplay cache. |
|
|
||||||
| `MTG_CLOAK_PORT` | `--cloak-port` | `443` | Which port we should use to connect to cloaked host in FakeTLS mode. |
|
|
||||||
| `MTG_MULTIPLEX_PERCONNECTION` | `--multiplex-per-connection` | `50` | How many client connections can share a single Telegram connection in adtag mode |
|
|
||||||
| `MTG_NTP_SERVERS` | `--ntp-server` | default pool | A list of NTP servers to use. |
|
|
||||||
| `MTG_PREFER_DIRECT_IP` | `--prefer-ip` | `ipv6` | Which IP protocol to prefer if possible. Works mostly in direct mode. |
|
|
||||||
|
|
||||||
|
|
||||||
Usually you want to modify only read/write buffer sizes. If you feel
|
|
||||||
that proxy is slow, try to increase both sizes giving more priority to
|
|
||||||
read buffer.
|
|
||||||
|
|
||||||
Unfortunately, MTPROTO proxy protocol does not allow us to use splice
|
|
||||||
or any other neat tricks how to eliminate the need of copying data into
|
|
||||||
userspace.
|
|
||||||
|
|
||||||
# How to run the tool
|
|
||||||
|
|
||||||
Now run the tool:
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg run <secret>
|
$ 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": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
How to run the tool with ADTag:
|
or if you are using docker:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg run <secret> <adtag>
|
$ docker exec mtg-proxy /mtg access /config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
This tool will listen on port 3128 by default with the given secret.
|
## Metrics
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
# oneliner to run this proxy
|
Here goes a list of metrics with their types but without a prefix.
|
||||||
|
|
||||||
Please ensure that docker is installed. After that just execute
|
| 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. |
|
||||||
|
| iplist_size | gauge | `ip_list` | A size of either allowlist or blocklist in use. |
|
||||||
|
| 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 | `ip_list` | Count of events when client connection was rejected because IP was found in the blocklist. |
|
||||||
|
| replay_attacks | counter | – | Count of detected replay attacks. |
|
||||||
|
|
||||||
```console
|
Tag meaning:
|
||||||
curl -sfL --compressed https://raw.githubusercontent.com/9seconds/mtg/master/run.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
|
| Name | Values | Description |
|
||||||
# statsd integration
|
|-------------|----------------------------|-----------------------------------------------|
|
||||||
|
| ip_family | `ipv4`, `ipv6` | A version of the IP protocol. |
|
||||||
mtg provides an integration with statsd, you can enable it with command
|
| dc | | A number of the Telegram DC for a connection. |
|
||||||
line interface. To enable it, you have to provide IP address of statsd
|
| telegram_ip | | IP address of the Telegram server. |
|
||||||
service.
|
| direction | `to_client`, `from_client` | A direction of the traffic flow. |
|
||||||
|
| ip_list | `allowlist`, `blocklist` | A type of the IP list. |
|
||||||
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` | number | The number of active connections. |
|
|
||||||
| `telegram_connections` | number | The number of active telegram connections. |
|
|
||||||
| `crashes` | number | An amount of crashes in client handlers. |
|
|
||||||
| `traffic.egress` | bytes | Traffic from the start of application. |
|
|
||||||
| `replay_attacks` | number | The number of prevented replay attacks. |
|
|
||||||
|
|
||||||
All metrics are prefixed with given prefix. Default prefix is `mtg`.
|
|
||||||
Also, metrics provide tags (ipv4/ipv6, dc indexes etc).
|
|
||||||
|
|
||||||
|
|
||||||
# Prometheus integration
|
|
||||||
|
|
||||||
[Prometheus](https://prometheus.io) integration comes out of
|
|
||||||
the box, you do not need to setup anything special.
|
|
||||||
|
|
||||||
|
|
||||||
# Upgrade to 1.0
|
|
||||||
|
|
||||||
Version 1.0 breaks compatibility with previous versions so please read
|
|
||||||
this chapter carefully:
|
|
||||||
|
|
||||||
1. mtg now uses subcommands. Please use `mtg run` instead of just
|
|
||||||
`mtg` to run a proxy.
|
|
||||||
2. Options which set host and port separately were removed in a
|
|
||||||
favor of fused `host:port` options.
|
|
||||||
3. Own stats server was removed. Prometheus endpoint is moved to
|
|
||||||
default stats endpoint.
|
|
||||||
4. It is possible to connect to this proxy only with a secret which
|
|
||||||
was used to run it. So, no backward compatibility of clients.
|
|
||||||
5. Multiplexing involves connectivity with middle proxies and involves
|
|
||||||
the most complex code path of this proxy. To avoid potential bugs,
|
|
||||||
we still recommend using direct mode.
|
|
||||||
|
|||||||
+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
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package antireplay
|
|
||||||
|
|
||||||
import "github.com/VictoriaMetrics/fastcache"
|
|
||||||
|
|
||||||
var (
|
|
||||||
prefixObfuscated2 = []byte{0x00}
|
|
||||||
prefixTLS = []byte{0x01}
|
|
||||||
)
|
|
||||||
|
|
||||||
type cache struct {
|
|
||||||
data *fastcache.Cache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) AddObfuscated2(data []byte) {
|
|
||||||
c.data.Set(keyObfuscated2(data), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) AddTLS(data []byte) {
|
|
||||||
c.data.Set(keyTLS(data), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) HasObfuscated2(data []byte) bool {
|
|
||||||
return c.data.Has(keyObfuscated2(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) HasTLS(data []byte) bool {
|
|
||||||
return c.data.Has(keyTLS(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyObfuscated2(data []byte) []byte {
|
|
||||||
return append(prefixObfuscated2, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyTLS(data []byte) []byte {
|
|
||||||
return append(prefixTLS, data...)
|
|
||||||
}
|
|
||||||
+13
-26
@@ -1,30 +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
|
package antireplay
|
||||||
|
|
||||||
import (
|
const (
|
||||||
"sync"
|
// DefaultStableBloomFilterMaxSize is a recommended byte size for a stable
|
||||||
|
// bloom filter.
|
||||||
|
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
|
||||||
|
|
||||||
"github.com/9seconds/mtg/config"
|
// DefaultStableBloomFilterErrorRate is a recommended default error rate for a
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
// stable bloom filter.
|
||||||
|
DefaultStableBloomFilterErrorRate = 0.001
|
||||||
)
|
)
|
||||||
|
|
||||||
type CacheInterface interface {
|
|
||||||
AddObfuscated2([]byte)
|
|
||||||
AddTLS([]byte)
|
|
||||||
HasObfuscated2([]byte) bool
|
|
||||||
HasTLS([]byte) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
Cache CacheInterface
|
|
||||||
initOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
initOnce.Do(func() {
|
|
||||||
if config.C.AntiReplayMaxSize == 0 {
|
|
||||||
Cache = nilCache{}
|
|
||||||
} else {
|
|
||||||
Cache = cache{fastcache.New(config.C.AntiReplayMaxSize)}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package antireplay
|
|
||||||
|
|
||||||
type nilCache struct{}
|
|
||||||
|
|
||||||
func (n nilCache) AddObfuscated2(_ []byte) {}
|
|
||||||
func (n nilCache) AddTLS(_ []byte) {}
|
|
||||||
func (n nilCache) HasObfuscated2(_ []byte) bool { return false }
|
|
||||||
func (n nilCache) HasTLS(_ []byte) bool { return false }
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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)
|
||||||
|
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{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"runtime/debug"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = "dev" // has to be set by ldflags
|
||||||
|
|
||||||
|
const (
|
||||||
|
buildInfoModuleStart byte = iota
|
||||||
|
buildInfoModuleFinish
|
||||||
|
buildInfoModuleDelimeter
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVersion() string {
|
||||||
|
buildInfo, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Now()
|
||||||
|
commit := ""
|
||||||
|
goVersion := buildInfo.GoVersion
|
||||||
|
dirtySuffix := ""
|
||||||
|
|
||||||
|
for _, setting := range buildInfo.Settings {
|
||||||
|
switch setting.Key {
|
||||||
|
case "vcs.time":
|
||||||
|
date, _ = time.Parse(time.RFC3339, setting.Value)
|
||||||
|
case "vcs.revision":
|
||||||
|
commit = setting.Value
|
||||||
|
case "vcs.modified":
|
||||||
|
if dirty, _ := strconv.ParseBool(setting.Value); dirty {
|
||||||
|
dirtySuffix = " [dirty]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
|
||||||
|
checksumModule := func(mod *debug.Module) {
|
||||||
|
hasher.Write([]byte{buildInfoModuleStart})
|
||||||
|
|
||||||
|
io.WriteString(hasher, mod.Path) //nolint: errcheck
|
||||||
|
hasher.Write([]byte{buildInfoModuleDelimeter})
|
||||||
|
|
||||||
|
io.WriteString(hasher, mod.Version) //nolint: errcheck
|
||||||
|
hasher.Write([]byte{buildInfoModuleDelimeter})
|
||||||
|
|
||||||
|
io.WriteString(hasher, mod.Sum) //nolint: errcheck
|
||||||
|
|
||||||
|
hasher.Write([]byte{buildInfoModuleFinish})
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(hasher, buildInfo.Path) //nolint: errcheck
|
||||||
|
|
||||||
|
binary.Write(hasher, binary.LittleEndian, uint64(1+len(buildInfo.Deps))) //nolint: errcheck
|
||||||
|
|
||||||
|
sort.Slice(buildInfo.Deps, func(i, j int) bool {
|
||||||
|
return buildInfo.Deps[i].Path > buildInfo.Deps[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
|
checksumModule(&buildInfo.Main)
|
||||||
|
|
||||||
|
for _, module := range buildInfo.Deps {
|
||||||
|
checksumModule(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (%s: %s on %s%s, modules checksum %s)",
|
||||||
|
version,
|
||||||
|
goVersion,
|
||||||
|
date.Format(time.RFC3339),
|
||||||
|
commit,
|
||||||
|
dirtySuffix,
|
||||||
|
base64.StdEncoding.EncodeToString(hasher.Sum(nil)))
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Generate(secretType, hostname string) {
|
|
||||||
data := make([]byte, config.SimpleSecretLength)
|
|
||||||
if _, err := rand.Read(data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret := hex.EncodeToString(data)
|
|
||||||
|
|
||||||
switch secretType {
|
|
||||||
case "simple":
|
|
||||||
PrintStdout(secret)
|
|
||||||
case "secured":
|
|
||||||
PrintStdout("dd" + secret)
|
|
||||||
default:
|
|
||||||
PrintStdout("ee" + secret + hex.EncodeToString([]byte(hostname)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/antireplay"
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
"github.com/9seconds/mtg/faketls"
|
|
||||||
"github.com/9seconds/mtg/hub"
|
|
||||||
"github.com/9seconds/mtg/ntp"
|
|
||||||
"github.com/9seconds/mtg/obfuscated2"
|
|
||||||
"github.com/9seconds/mtg/proxy"
|
|
||||||
"github.com/9seconds/mtg/stats"
|
|
||||||
"github.com/9seconds/mtg/telegram"
|
|
||||||
"github.com/9seconds/mtg/utils"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Proxy() error { // nolint: funlen
|
|
||||||
ctx := utils.GetSignalContext()
|
|
||||||
|
|
||||||
atom := zap.NewAtomicLevel()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case config.C.Debug:
|
|
||||||
atom.SetLevel(zapcore.DebugLevel)
|
|
||||||
case config.C.Verbose:
|
|
||||||
atom.SetLevel(zapcore.InfoLevel)
|
|
||||||
default:
|
|
||||||
atom.SetLevel(zapcore.ErrorLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
encoderCfg := zap.NewProductionEncoderConfig()
|
|
||||||
logger := zap.New(zapcore.NewCore(
|
|
||||||
zapcore.NewJSONEncoder(encoderCfg),
|
|
||||||
zapcore.Lock(os.Stderr),
|
|
||||||
atom,
|
|
||||||
))
|
|
||||||
|
|
||||||
zap.ReplaceGlobals(logger)
|
|
||||||
defer logger.Sync() // nolint: errcheck
|
|
||||||
|
|
||||||
if err := config.InitPublicAddress(ctx); err != nil {
|
|
||||||
Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.S().Debugw("Configuration", "config", config.Printable())
|
|
||||||
|
|
||||||
if config.C.MiddleProxyMode() {
|
|
||||||
zap.S().Infow("Use middle proxy connection to Telegram")
|
|
||||||
|
|
||||||
diff, err := ntp.Fetch()
|
|
||||||
if err != nil {
|
|
||||||
Fatal("Cannot fetch time data from NTP")
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff > time.Second {
|
|
||||||
Fatal("Your local time is skewed and drift is bigger than a second. Please sync your time.")
|
|
||||||
}
|
|
||||||
|
|
||||||
go ntp.AutoUpdate()
|
|
||||||
} else {
|
|
||||||
zap.S().Infow("Use direct connection to Telegram")
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintJSONStdout(config.GetURLs())
|
|
||||||
|
|
||||||
if err := stats.Init(ctx); err != nil {
|
|
||||||
Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
antireplay.Init()
|
|
||||||
telegram.Init()
|
|
||||||
hub.Init(ctx)
|
|
||||||
|
|
||||||
proxyListener, err := net.Listen("tcp", config.C.Bind.String())
|
|
||||||
if err != nil {
|
|
||||||
Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
proxyListener.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
app := &proxy.Proxy{
|
|
||||||
Logger: zap.S().Named("proxy"),
|
|
||||||
Context: ctx,
|
|
||||||
ClientProtocolMaker: obfuscated2.MakeClientProtocol,
|
|
||||||
}
|
|
||||||
if config.C.SecretMode == config.SecretModeTLS {
|
|
||||||
app.ClientProtocolMaker = faketls.MakeClientProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Serve(proxyListener)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Fatal(arg interface{}) {
|
|
||||||
if value, ok := arg.(error); ok {
|
|
||||||
arg = fmt.Errorf("fatal error: %+v", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintStderr(arg)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintStderr(args ...interface{}) {
|
|
||||||
fmt.Fprintln(os.Stderr, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintStdout(args ...interface{}) {
|
|
||||||
fmt.Println(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintJSONStderr(data interface{}) {
|
|
||||||
printJSON(os.Stderr, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintJSONStdout(data interface{}) {
|
|
||||||
printJSON(os.Stdout, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func printJSON(writer io.Writer, data interface{}) {
|
|
||||||
encoder := json.NewEncoder(writer)
|
|
||||||
encoder.SetEscapeHTML(false)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/alecthomas/units"
|
|
||||||
statsd "github.com/smira/go-statsd"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SecretMode uint8
|
|
||||||
|
|
||||||
func (s SecretMode) String() string {
|
|
||||||
switch s {
|
|
||||||
case SecretModeSimple:
|
|
||||||
return "simple"
|
|
||||||
case SecretModeSecured:
|
|
||||||
return "secured"
|
|
||||||
case SecretModeTLS:
|
|
||||||
return "tls"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "tls"
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
SecretModeSimple SecretMode = iota
|
|
||||||
SecretModeSecured
|
|
||||||
SecretModeTLS
|
|
||||||
)
|
|
||||||
|
|
||||||
type PreferIP uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
PreferIPv4 PreferIP = iota
|
|
||||||
PreferIPv6
|
|
||||||
)
|
|
||||||
|
|
||||||
const SimpleSecretLength = 16
|
|
||||||
|
|
||||||
type OptionType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
OptionTypeDebug OptionType = iota
|
|
||||||
OptionTypeVerbose
|
|
||||||
|
|
||||||
OptionTypePreferIP
|
|
||||||
|
|
||||||
OptionTypeBind
|
|
||||||
OptionTypePublicIPv4
|
|
||||||
OptionTypePublicIPv6
|
|
||||||
|
|
||||||
OptionTypeStatsBind
|
|
||||||
OptionTypeStatsNamespace
|
|
||||||
OptionTypeStatsdAddress
|
|
||||||
OptionTypeStatsdTagsFormat
|
|
||||||
OptionTypeStatsdTags
|
|
||||||
|
|
||||||
OptionTypeWriteBufferSize
|
|
||||||
OptionTypeReadBufferSize
|
|
||||||
|
|
||||||
OptionTypeCloakPort
|
|
||||||
|
|
||||||
OptionTypeAntiReplayMaxSize
|
|
||||||
|
|
||||||
OptionTypeMultiplexPerConnection
|
|
||||||
|
|
||||||
OptionTypeNTPServers
|
|
||||||
|
|
||||||
OptionTypeSecret
|
|
||||||
OptionTypeAdtag
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Bind *net.TCPAddr `json:"bind"`
|
|
||||||
PublicIPv4 *net.TCPAddr `json:"public_ipv4"`
|
|
||||||
PublicIPv6 *net.TCPAddr `json:"public_ipv6"`
|
|
||||||
StatsBind *net.TCPAddr `json:"stats_bind"`
|
|
||||||
StatsdAddr *net.TCPAddr `json:"stats_addr"`
|
|
||||||
StatsdTagsFormat *statsd.TagFormat `json:"statsd_tags_format"`
|
|
||||||
|
|
||||||
StatsNamespace string `json:"stats_namespace"`
|
|
||||||
CloakHost string `json:"cloak_host"`
|
|
||||||
StatsdTags map[string]string `json:"statsd_tags"`
|
|
||||||
|
|
||||||
WriteBuffer int `json:"write_buffer"`
|
|
||||||
ReadBuffer int `json:"read_buffer"`
|
|
||||||
CloakPort int `json:"cloak_port"`
|
|
||||||
|
|
||||||
AntiReplayMaxSize int `json:"anti_replay_max_size"`
|
|
||||||
|
|
||||||
MultiplexPerConnection int `json:"multiplex_per_connection"`
|
|
||||||
|
|
||||||
Debug bool `json:"debug"`
|
|
||||||
Verbose bool `json:"verbose"`
|
|
||||||
SecretMode SecretMode `json:"secret_mode"`
|
|
||||||
PreferIP PreferIP `json:"prefer_ip"`
|
|
||||||
NTPServers []string `json:"ntp_servers"`
|
|
||||||
|
|
||||||
Secret []byte `json:"secret"`
|
|
||||||
AdTag []byte `json:"adtag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ClientReadBuffer() int {
|
|
||||||
return c.ReadBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ClientWriteBuffer() int {
|
|
||||||
return c.WriteBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) MiddleProxyMode() bool {
|
|
||||||
return len(c.AdTag) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ProxyReadBuffer() int {
|
|
||||||
value := c.ReadBuffer
|
|
||||||
|
|
||||||
if c.MiddleProxyMode() {
|
|
||||||
value = c.adjustProxyValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ProxyWriteBuffer() int {
|
|
||||||
value := c.WriteBuffer
|
|
||||||
|
|
||||||
if c.MiddleProxyMode() {
|
|
||||||
value = c.adjustProxyValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) adjustProxyValue(value int) int {
|
|
||||||
if c.MultiplexPerConnection == 0 {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
fvalue := float64(value)
|
|
||||||
|
|
||||||
newValue := fvalue * 2 * math.Log(float64(c.MultiplexPerConnection))
|
|
||||||
newValue = math.Ceil(newValue)
|
|
||||||
newValue = math.Max(fvalue, newValue)
|
|
||||||
|
|
||||||
return int(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Opt struct {
|
|
||||||
Option OptionType
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var C = Config{}
|
|
||||||
|
|
||||||
func Init(options ...Opt) error { // nolint: gocyclo, funlen
|
|
||||||
for _, opt := range options {
|
|
||||||
switch opt.Option {
|
|
||||||
case OptionTypeDebug:
|
|
||||||
C.Debug = opt.Value.(bool)
|
|
||||||
case OptionTypeVerbose:
|
|
||||||
C.Verbose = opt.Value.(bool)
|
|
||||||
case OptionTypePreferIP:
|
|
||||||
value := opt.Value.(string)
|
|
||||||
switch value {
|
|
||||||
case "ipv4":
|
|
||||||
C.PreferIP = PreferIPv4
|
|
||||||
case "ipv6":
|
|
||||||
C.PreferIP = PreferIPv6
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("incorrect direct IP mode %s", value)
|
|
||||||
}
|
|
||||||
case OptionTypeBind:
|
|
||||||
C.Bind = opt.Value.(*net.TCPAddr)
|
|
||||||
case OptionTypePublicIPv4:
|
|
||||||
C.PublicIPv4 = opt.Value.(*net.TCPAddr)
|
|
||||||
if C.PublicIPv4 == nil {
|
|
||||||
C.PublicIPv4 = &net.TCPAddr{}
|
|
||||||
}
|
|
||||||
case OptionTypePublicIPv6:
|
|
||||||
C.PublicIPv6 = opt.Value.(*net.TCPAddr)
|
|
||||||
if C.PublicIPv6 == nil {
|
|
||||||
C.PublicIPv6 = &net.TCPAddr{}
|
|
||||||
}
|
|
||||||
case OptionTypeStatsBind:
|
|
||||||
C.StatsBind = opt.Value.(*net.TCPAddr)
|
|
||||||
case OptionTypeStatsNamespace:
|
|
||||||
C.StatsNamespace = opt.Value.(string)
|
|
||||||
case OptionTypeStatsdAddress:
|
|
||||||
C.StatsdAddr = opt.Value.(*net.TCPAddr)
|
|
||||||
case OptionTypeStatsdTagsFormat:
|
|
||||||
value := opt.Value.(string)
|
|
||||||
switch value {
|
|
||||||
case "datadog":
|
|
||||||
C.StatsdTagsFormat = statsd.TagFormatDatadog
|
|
||||||
case "influxdb":
|
|
||||||
C.StatsdTagsFormat = statsd.TagFormatInfluxDB
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("incorrect statsd tag %s", value)
|
|
||||||
}
|
|
||||||
case OptionTypeStatsdTags:
|
|
||||||
C.StatsdTags = opt.Value.(map[string]string)
|
|
||||||
case OptionTypeWriteBufferSize:
|
|
||||||
C.WriteBuffer = int(opt.Value.(units.Base2Bytes))
|
|
||||||
case OptionTypeReadBufferSize:
|
|
||||||
C.ReadBuffer = int(opt.Value.(units.Base2Bytes))
|
|
||||||
case OptionTypeCloakPort:
|
|
||||||
C.CloakPort = int(opt.Value.(uint16))
|
|
||||||
case OptionTypeAntiReplayMaxSize:
|
|
||||||
C.AntiReplayMaxSize = int(opt.Value.(units.Base2Bytes))
|
|
||||||
case OptionTypeMultiplexPerConnection:
|
|
||||||
C.MultiplexPerConnection = int(opt.Value.(uint))
|
|
||||||
case OptionTypeNTPServers:
|
|
||||||
C.NTPServers = opt.Value.([]string)
|
|
||||||
if len(C.NTPServers) == 0 {
|
|
||||||
return errors.New("ntp server list is empty")
|
|
||||||
}
|
|
||||||
case OptionTypeSecret:
|
|
||||||
C.Secret = opt.Value.([]byte)
|
|
||||||
case OptionTypeAdtag:
|
|
||||||
C.AdTag = opt.Value.([]byte)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown tag %v", opt.Option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(C.Secret) == 1+SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xdd}):
|
|
||||||
C.SecretMode = SecretModeSecured
|
|
||||||
C.Secret = bytes.TrimPrefix(C.Secret, []byte{0xdd})
|
|
||||||
case len(C.Secret) > SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xee}):
|
|
||||||
C.SecretMode = SecretModeTLS
|
|
||||||
secret := bytes.TrimPrefix(C.Secret, []byte{0xee})
|
|
||||||
C.Secret = secret[:SimpleSecretLength]
|
|
||||||
C.CloakHost = string(secret[SimpleSecretLength:])
|
|
||||||
case len(C.Secret) == SimpleSecretLength:
|
|
||||||
C.SecretMode = SecretModeSimple
|
|
||||||
default:
|
|
||||||
return errors.New("incorrect secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.MultiplexPerConnection == 0 {
|
|
||||||
return errors.New("cannot use 0 clients per connection for multiplexing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.CloakHost != "" {
|
|
||||||
if _, err := net.LookupHost(C.CloakHost); err != nil {
|
|
||||||
zap.S().Warnw("Cannot resolve address of host", "hostname", C.CloakHost, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitPublicAddress(ctx context.Context) error {
|
|
||||||
if C.PublicIPv4.Port == 0 {
|
|
||||||
C.PublicIPv4.Port = C.Bind.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.PublicIPv6.Port == 0 {
|
|
||||||
C.PublicIPv6.Port = C.Bind.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
foundAddress := C.PublicIPv4.IP != nil || C.PublicIPv6.IP != nil
|
|
||||||
|
|
||||||
if C.PublicIPv4.IP == nil {
|
|
||||||
ip, err := getGlobalIPv4(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Warnw("Cannot resolve public address", "error", err)
|
|
||||||
} else {
|
|
||||||
C.PublicIPv4.IP = ip
|
|
||||||
foundAddress = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.PublicIPv6.IP == nil {
|
|
||||||
ip, err := getGlobalIPv6(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Warnw("Cannot resolve public address", "error", err)
|
|
||||||
} else {
|
|
||||||
C.PublicIPv6.IP = ip
|
|
||||||
foundAddress = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundAddress {
|
|
||||||
return errors.New("cannot resolve any public address")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Printable() interface{} {
|
|
||||||
data, err := json.Marshal(C)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rv := map[string]interface{}{}
|
|
||||||
if err := json.Unmarshal(data, &rv); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ifconfigAddress = "https://ifconfig.co/ip"
|
|
||||||
ifconfigTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func getGlobalIPv4(ctx context.Context) (net.IP, error) {
|
|
||||||
ip, err := fetchIP(ctx, "tcp4")
|
|
||||||
if err != nil || ip.To4() == nil {
|
|
||||||
return nil, fmt.Errorf("cannot find public ipv4 address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGlobalIPv6(ctx context.Context) (net.IP, error) {
|
|
||||||
ip, err := fetchIP(ctx, "tcp6")
|
|
||||||
if err != nil || ip.To4() != nil {
|
|
||||||
return nil, fmt.Errorf("cannot find public ipv6 address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIP(ctx context.Context, network string) (net.IP, error) {
|
|
||||||
dialer := &net.Dialer{FallbackDelay: -1}
|
|
||||||
client := &http.Client{
|
|
||||||
Jar: nil,
|
|
||||||
Timeout: ifconfigTimeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, addr)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", ifconfigAddress, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create a request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
if resp != nil {
|
|
||||||
io.Copy(ioutil.Discard, resp.Body) // nolint: errcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("cannot perform a request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respDataBytes, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot read response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
respData := strings.TrimSpace(string(respDataBytes))
|
|
||||||
|
|
||||||
ip := net.ParseIP(respData)
|
|
||||||
if ip == nil {
|
|
||||||
return nil, fmt.Errorf("ifconfig.co returns incorrect IP %s", respData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type URLs struct {
|
|
||||||
TG string `json:"tg_url"`
|
|
||||||
TMe string `json:"tme_url"`
|
|
||||||
TGQRCode string `json:"tg_qrcode"`
|
|
||||||
TMeQRCode string `json:"tme_qrcode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPURLs struct {
|
|
||||||
IPv4 URLs `json:"ipv4"`
|
|
||||||
IPv6 URLs `json:"ipv6"`
|
|
||||||
BotSecret string `json:"secret_for_mtproxybot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetURLs() (urls IPURLs) {
|
|
||||||
secret := ""
|
|
||||||
|
|
||||||
switch C.SecretMode {
|
|
||||||
case SecretModeSimple:
|
|
||||||
secret = hex.EncodeToString(C.Secret)
|
|
||||||
case SecretModeSecured:
|
|
||||||
secret = "dd" + hex.EncodeToString(C.Secret)
|
|
||||||
case SecretModeTLS:
|
|
||||||
secret = "ee" + hex.EncodeToString(C.Secret) + hex.EncodeToString([]byte(C.CloakHost))
|
|
||||||
}
|
|
||||||
|
|
||||||
urls.IPv4 = makeURLs(C.PublicIPv4, secret)
|
|
||||||
urls.IPv6 = makeURLs(C.PublicIPv6, secret)
|
|
||||||
urls.BotSecret = hex.EncodeToString(C.Secret)
|
|
||||||
|
|
||||||
return urls
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeURLs(addr *net.TCPAddr, secret string) (urls URLs) {
|
|
||||||
values := url.Values{}
|
|
||||||
values.Set("server", addr.IP.String())
|
|
||||||
values.Set("port", strconv.Itoa(addr.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 {
|
|
||||||
qr := 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)
|
|
||||||
qr.RawQuery = values.Encode()
|
|
||||||
|
|
||||||
return qr.String()
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type ConnectionAcks struct {
|
|
||||||
Simple bool
|
|
||||||
Quick bool
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type DC int16
|
|
||||||
|
|
||||||
const DCDefaultIdx DC = 1
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ConnIDLength = 8
|
|
||||||
|
|
||||||
type ConnID [ConnIDLength]byte
|
|
||||||
|
|
||||||
func (c ConnID) String() string {
|
|
||||||
return hex.EncodeToString(c[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnID() ConnID {
|
|
||||||
var id ConnID
|
|
||||||
|
|
||||||
if _, err := rand.Read(id[:]); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type Packet []byte
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type ConnectionProtocol uint8
|
|
||||||
|
|
||||||
func (c ConnectionProtocol) String() string {
|
|
||||||
switch c {
|
|
||||||
case ConnectionProtocolAny:
|
|
||||||
return "any"
|
|
||||||
case ConnectionProtocolIPv4:
|
|
||||||
return "ipv4"
|
|
||||||
case ConnectionProtocolIPv6:
|
|
||||||
return "ipv6"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "ipv6"
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConnectionProtocolIPv4 ConnectionProtocol = 1
|
|
||||||
ConnectionProtocolIPv6 = ConnectionProtocolIPv4 << 1
|
|
||||||
ConnectionProtocolAny = ConnectionProtocolIPv4 | ConnectionProtocolIPv6
|
|
||||||
)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type ConnectionType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConnectionTypeUnknown ConnectionType = iota
|
|
||||||
ConnectionTypeAbridged
|
|
||||||
ConnectionTypeIntermediate
|
|
||||||
ConnectionTypeSecure
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ConnectionTagAbridged = []byte{0xef, 0xef, 0xef, 0xef}
|
|
||||||
ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee}
|
|
||||||
ConnectionTagSecure = []byte{0xdd, 0xdd, 0xdd, 0xdd}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t ConnectionType) Tag() []byte {
|
|
||||||
switch t {
|
|
||||||
case ConnectionTypeAbridged:
|
|
||||||
return ConnectionTagAbridged
|
|
||||||
case ConnectionTypeIntermediate:
|
|
||||||
return ConnectionTagIntermediate
|
|
||||||
case ConnectionTypeSecure, ConnectionTypeUnknown:
|
|
||||||
return ConnectionTagSecure
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConnectionTagSecure
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Wrap interface {
|
|
||||||
Conn() net.Conn
|
|
||||||
Logger() *zap.SugaredLogger
|
|
||||||
LocalAddr() *net.TCPAddr
|
|
||||||
RemoteAddr() *net.TCPAddr
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type PacketAckReader interface {
|
|
||||||
Read(*ConnectionAcks) (Packet, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckWriter interface {
|
|
||||||
Write(Packet, *ConnectionAcks) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckCloser interface {
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckReadCloser interface {
|
|
||||||
PacketAckReader
|
|
||||||
PacketAckCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckWriteCloser interface {
|
|
||||||
PacketAckWriter
|
|
||||||
PacketAckCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckReadWriter interface {
|
|
||||||
PacketAckReader
|
|
||||||
PacketAckWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckReadWriteCloser interface {
|
|
||||||
PacketAckReader
|
|
||||||
PacketAckWriter
|
|
||||||
PacketAckCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckFullReadWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
PacketAckReadWriteCloser
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type BasePacketReader interface {
|
|
||||||
Read() (Packet, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BasePacketWriter interface {
|
|
||||||
Write(Packet) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReader interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketReader
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketWriter interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReadCloser interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketReader
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReadWriter interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
BasePacketReader
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReadWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
BasePacketReader
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BaseStreamReaderWithTimeout interface {
|
|
||||||
ReadTimeout([]byte, time.Duration) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseStreamWriterWithTimeout interface {
|
|
||||||
WriteTimeout([]byte, time.Duration) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReader interface {
|
|
||||||
Wrap
|
|
||||||
io.Reader
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamWriter interface {
|
|
||||||
Wrap
|
|
||||||
io.Writer
|
|
||||||
BaseStreamWriterWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReadCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.ReadCloser
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.WriteCloser
|
|
||||||
BaseStreamWriterWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReadWriter interface {
|
|
||||||
Wrap
|
|
||||||
io.ReadWriter
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReadWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.ReadWriteCloser
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
BaseStreamWriterWithTimeout
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package essentials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloseableReader is an [io.Reader] interface that can close its reading end.
|
||||||
|
type CloseableReader interface {
|
||||||
|
io.Reader
|
||||||
|
CloseRead() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseableWriter is an [io.Writer] that can close its writing end.
|
||||||
|
type CloseableWriter interface {
|
||||||
|
io.Writer
|
||||||
|
CloseWrite() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conn is an extension of [net.Conn] that can close its ends. This mostly
|
||||||
|
// implies TCP connections.
|
||||||
|
type Conn interface {
|
||||||
|
net.Conn
|
||||||
|
CloseableReader
|
||||||
|
CloseableWriter
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// This is a minimal package that contains _essentials_ of mtglib and its
|
||||||
|
// complimentary packages. This is mostly required to comply some interfaces
|
||||||
|
// between mtglib and its internals to avoid circular dependencies.
|
||||||
|
//
|
||||||
|
// This package should contain only bare minimum and mostly technical.
|
||||||
|
package essentials
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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)
|
||||||
|
case mtglib.EventIPListSize:
|
||||||
|
observer.EventIPListSize(typedEvt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
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) TestEventIPListSize() {
|
||||||
|
evt := mtglib.NewEventIPListSize(10, true)
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventIPListSize", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventIPListSize)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
suite.Equal(evt.Size, caught.Size)
|
||||||
|
suite.Equal(evt.IsBlockList, caught.IsBlockList)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,72 @@
|
|||||||
|
// 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)
|
||||||
|
|
||||||
|
// EventIPListSize reacts on incoming mtglib.EventIPListSize
|
||||||
|
EventIPListSize(mtglib.EventIPListSize)
|
||||||
|
|
||||||
|
// 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,50 @@
|
|||||||
|
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) EventIPListSize(evt mtglib.EventIPListSize) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) Shutdown() {
|
||||||
|
o.Called()
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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) EventIPListSize(evt mtglib.EventIPListSize) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(m.observers))
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
go func(obs Observer) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
obs.EventIPListSize(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,34 @@
|
|||||||
|
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) EventIPListSize(_ mtglib.EventIPListSize) {}
|
||||||
|
func (n noopObserver) Shutdown() {}
|
||||||
|
|
||||||
|
// NewNoopObserver creates an observer which discards each message.
|
||||||
|
func NewNoopObserver() Observer {
|
||||||
|
return noopObserver{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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"),
|
||||||
|
"ip-list-size": mtglib.NewEventIPListSize(10, true),
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
case mtglib.EventIPListSize:
|
||||||
|
observer.EventIPListSize(typedEvt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoop(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &NoopTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Deprecated: this setting is no longer makes any effect.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Telegram uses different DCs for different purposes. Unfortunately, most of
|
||||||
|
# DCs are not public, and dependent on a location of the current user, so
|
||||||
|
# mtg cannot know upfront about all of them, and how to access them. It has
|
||||||
|
# a default list of DCs, including some CDN IPs, but it is possible that some
|
||||||
|
# of them are not working for you. In this case, you can override them here.
|
||||||
|
[[dc-overrides]]
|
||||||
|
dc = 101
|
||||||
|
ips = ["127.0.0.1:443"]
|
||||||
|
|
||||||
|
# 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 Cloudflare.
|
||||||
|
doh-ip = "1.1.1.1"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Allowlist is an opposite to a blocklist. Only those IPs that are coming from
|
||||||
|
# subnets defined in these lists are allowed. All others will be rejected.
|
||||||
|
#
|
||||||
|
# If this feature is disabled, then there won't be any check performed by this
|
||||||
|
# validator. It is possible to combine both blocklist and whitelist.
|
||||||
|
[defense.allowlist]
|
||||||
|
# You can enable/disable this feature.
|
||||||
|
enabled = false
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
]
|
||||||
|
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"
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/antireplay"
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/obfuscated2"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
"github.com/9seconds/mtg/stats"
|
|
||||||
"github.com/9seconds/mtg/tlstypes"
|
|
||||||
"github.com/9seconds/mtg/wrappers/stream"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClientProtocol struct {
|
|
||||||
obfuscated2.ClientProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) {
|
|
||||||
rewinded := stream.NewRewind(socket)
|
|
||||||
bufferedReader := bufio.NewReader(rewinded)
|
|
||||||
|
|
||||||
for _, expected := range faketlsStartBytes {
|
|
||||||
if actual, err := bufferedReader.ReadByte(); err != nil || actual != expected {
|
|
||||||
rewinded.Rewind()
|
|
||||||
c.cloakHost(rewinded)
|
|
||||||
|
|
||||||
return nil, errors.New("failed first bytes of tls handshake")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rewinded.Rewind()
|
|
||||||
rewinded = stream.NewRewind(rewinded)
|
|
||||||
|
|
||||||
if err := c.tlsHandshake(rewinded); err != nil {
|
|
||||||
rewinded.Rewind()
|
|
||||||
c.cloakHost(rewinded)
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed tls handshake: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := stream.NewFakeTLS(socket)
|
|
||||||
|
|
||||||
conn, err := c.ClientProtocol.Handshake(conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientProtocol) tlsHandshake(conn io.ReadWriter) error {
|
|
||||||
helloRecord, err := tlstypes.ReadRecord(conn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot read initial record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
helloRecord.Data.WriteBytes(buf)
|
|
||||||
|
|
||||||
clientHello, err := tlstypes.ParseClientHello(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse client hello: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
digest := clientHello.Digest()
|
|
||||||
for i := 0; i < len(digest)-4; i++ {
|
|
||||||
if digest[i] != 0 {
|
|
||||||
return errBadDigest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp := int64(binary.LittleEndian.Uint32(digest[len(digest)-4:]))
|
|
||||||
createdAt := time.Unix(timestamp, 0)
|
|
||||||
timeDiff := time.Since(createdAt)
|
|
||||||
|
|
||||||
if (timeDiff > TimeSkew || timeDiff < -TimeSkew) && timestamp > TimeFromBoot {
|
|
||||||
return errBadTime
|
|
||||||
}
|
|
||||||
|
|
||||||
if antireplay.Cache.HasTLS(clientHello.Random[:]) {
|
|
||||||
stats.Stats.ReplayDetected()
|
|
||||||
|
|
||||||
return errors.New("replay attack is detected")
|
|
||||||
}
|
|
||||||
|
|
||||||
antireplay.Cache.AddTLS(clientHello.Random[:])
|
|
||||||
serverHello := tlstypes.NewServerHello(clientHello)
|
|
||||||
serverHelloPacket := serverHello.WelcomePacket()
|
|
||||||
|
|
||||||
if _, err := conn.Write(serverHelloPacket); err != nil {
|
|
||||||
return fmt.Errorf("cannot send welcome packet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientProtocol) cloakHost(clientConn io.ReadWriteCloser) {
|
|
||||||
stats.Stats.CloakedRequest()
|
|
||||||
|
|
||||||
addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort))
|
|
||||||
|
|
||||||
hostConn, err := net.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cloak(clientConn, hostConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeClientProtocol() protocol.ClientProtocol {
|
|
||||||
return &ClientProtocol{}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/wrappers/rwc"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cloakLastActivityTimeout = 5 * time.Second
|
|
||||||
cloakMaxTimeout = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func cloak(one, another io.ReadWriteCloser) {
|
|
||||||
defer func() {
|
|
||||||
one.Close()
|
|
||||||
another.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
channelPing := make(chan struct{}, 1)
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
one = rwc.NewPing(ctx, one, channelPing)
|
|
||||||
another = rwc.NewPing(ctx, another, channelPing)
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
|
|
||||||
wg.Add(2)
|
|
||||||
|
|
||||||
go cloakPipe(one, another, wg)
|
|
||||||
|
|
||||||
go cloakPipe(another, one, wg)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
lastActivityTimer := time.NewTimer(cloakLastActivityTimeout)
|
|
||||||
defer lastActivityTimer.Stop()
|
|
||||||
|
|
||||||
maxTimer := time.NewTimer(cloakMaxTimeout)
|
|
||||||
defer maxTimer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-channelPing:
|
|
||||||
lastActivityTimer.Stop()
|
|
||||||
lastActivityTimer = time.NewTimer(cloakLastActivityTimeout)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-lastActivityTimer.C:
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
return
|
|
||||||
case <-maxTimer.C:
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloakPipe(one io.Writer, another io.Reader, wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
io.Copy(one, another) // nolint: errcheck
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TimeSkew = 5 * time.Second
|
|
||||||
TimeFromBoot = 24 * 60 * 60
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errBadDigest = errors.New("bad digest")
|
|
||||||
errBadTime = errors.New("bad time")
|
|
||||||
|
|
||||||
faketlsStartBytes = [...]byte{
|
|
||||||
0x16,
|
|
||||||
0x03,
|
|
||||||
0x01,
|
|
||||||
0x02,
|
|
||||||
0x00,
|
|
||||||
0x01,
|
|
||||||
0x00,
|
|
||||||
0x01,
|
|
||||||
0xfc,
|
|
||||||
0x03,
|
|
||||||
0x03,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,22 +1,53 @@
|
|||||||
module github.com/9seconds/mtg
|
module github.com/9seconds/mtg/v2
|
||||||
|
|
||||||
go 1.13
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VictoriaMetrics/fastcache v1.5.7
|
github.com/OneOfOne/xxhash v1.2.8
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d
|
github.com/alecthomas/kong v1.14.0
|
||||||
github.com/beevik/ntp v0.3.0
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
|
||||||
github.com/prometheus/client_golang v1.7.1
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/prometheus/common v0.13.0 // indirect
|
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
|
||||||
github.com/smira/go-statsd v1.3.1
|
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
github.com/jarcoal/httpmock v1.0.8
|
||||||
go.uber.org/zap v1.16.0
|
github.com/mccutchen/go-httpbin v1.1.1
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
|
github.com/panjf2000/ants/v2 v2.11.5
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
github.com/prometheus/client_golang v1.23.2
|
||||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
golang.org/x/tools v0.0.0-20200319210407-521f4a0cd458 // indirect
|
github.com/rs/zerolog v1.34.0
|
||||||
google.golang.org/protobuf v1.25.0 // indirect
|
github.com/smira/go-statsd v1.3.4
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3 // indirect
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
|
||||||
|
github.com/yl2chen/cidranger v1.0.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,487 +1,156 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
|
||||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
||||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
|
||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
|
||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
|
||||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
|
||||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
|
||||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
|
||||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
|
||||||
github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
|
|
||||||
github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
|
||||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
|
||||||
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
|
||||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
|
||||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
|
||||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
|
||||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/panjf2000/ants/v2 v2.11.5 h1:a7LMnMEeux/ebqTux140tRiaqcFTV0q2bEHF03nl6Rg=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/panjf2000/ants/v2 v2.11.5/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
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 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
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 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
|
||||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
|
||||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
|
||||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
|
||||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
|
||||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
|
||||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
|
||||||
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.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
|
||||||
github.com/kisielk/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/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
|
||||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
|
||||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
|
||||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
|
||||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
|
||||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
|
||||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
|
||||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
|
||||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
|
||||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
|
||||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
|
||||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
|
||||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
|
||||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
|
||||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
|
||||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
|
||||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
|
||||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
|
||||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
|
||||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
|
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
|
||||||
github.com/prometheus/common v0.13.0 h1:vJlpe9wPgDRM1Z+7Wj3zUUjY1nr6/1jNKyl7llliccg=
|
|
||||||
github.com/prometheus/common v0.13.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
|
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
|
||||||
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
|
|
||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
|
||||||
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
|
|
||||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
|
||||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
|
||||||
github.com/smira/go-statsd v1.3.1 h1:JalGiHNdK7GqVAPpg7j0Kwp2jZrz/fCg/B4ZuNuBY2w=
|
|
||||||
github.com/smira/go-statsd v1.3.1/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
|
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
|
||||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
|
||||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
|
||||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
||||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae h1:ArVM1jICfm7g4E4dBet+KHUFMLuxmj1Nxdp/tr3ByCU=
|
||||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae/go.mod h1:cldYm15/XHcGt7ndItnEWHwFZo7dinU+2QoyjfErhsI=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e h1:xA7GVlbz6teIF4FdvuqwbX6C4tiqNk2PH7FRPIDerao=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b h1:p+bJ3v5uUdEVMCoeFUs+BNJPsqt+Y6BLbDaPfTcbcH8=
|
||||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
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/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|
||||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
|
||||||
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-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
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-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
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/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
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/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 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
|
||||||
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-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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/text v0.3.0/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20191119224855-298f0cb1881e/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.0.0-20200319210407-521f4a0cd458 h1:DgonIcqC7u+gVZX7lpuReBil5B/i8fvW/hAQdhT6/ao=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
golang.org/x/tools v0.0.0-20200319210407-521f4a0cd458/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
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/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 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
|
||||||
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.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
|
||||||
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
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/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
|
||||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/mtproto"
|
|
||||||
"github.com/9seconds/mtg/mtproto/rpc"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
const connectionTTL = time.Hour
|
|
||||||
|
|
||||||
type connection struct {
|
|
||||||
conn conntypes.PacketReadWriteCloser
|
|
||||||
proxyConns map[string]*ProxyConn
|
|
||||||
closeOnce sync.Once
|
|
||||||
proxyConnsMutex sync.RWMutex
|
|
||||||
id int
|
|
||||||
logger *zap.SugaredLogger
|
|
||||||
|
|
||||||
channelDone chan struct{}
|
|
||||||
channelWrite chan conntypes.Packet
|
|
||||||
channelRead chan *rpc.ProxyResponse
|
|
||||||
channelConnAttach chan *ProxyConn
|
|
||||||
channelConnDetach chan conntypes.ConnID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) run() {
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
ttl := time.NewTimer(connectionTTL)
|
|
||||||
defer ttl.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
for _, v := range c.proxyConns {
|
|
||||||
v.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
case <-ttl.C:
|
|
||||||
c.logger.Debugw("Closing connection by TTL")
|
|
||||||
c.Close()
|
|
||||||
case resp := <-c.channelRead:
|
|
||||||
if channel, ok := c.proxyConns[string(resp.ConnID[:])]; ok {
|
|
||||||
if resp.Type == rpc.ProxyResponseTypeCloseExt {
|
|
||||||
channel.Close()
|
|
||||||
} else {
|
|
||||||
channel.put(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case packet := <-c.channelWrite:
|
|
||||||
if err := c.conn.Write(packet); err != nil {
|
|
||||||
c.logger.Debugw("Cannot write packet", "error", err)
|
|
||||||
c.Close()
|
|
||||||
}
|
|
||||||
case conn := <-c.channelConnAttach:
|
|
||||||
c.proxyConnsMutex.Lock()
|
|
||||||
c.proxyConns[string(conn.req.ConnID[:])] = conn
|
|
||||||
c.proxyConnsMutex.Unlock()
|
|
||||||
conn.channelWrite = c.channelWrite
|
|
||||||
case connID := <-c.channelConnDetach:
|
|
||||||
if conn, ok := c.proxyConns[string(connID[:])]; ok {
|
|
||||||
c.proxyConnsMutex.Lock()
|
|
||||||
delete(c.proxyConns, string(connID[:]))
|
|
||||||
c.proxyConnsMutex.Unlock()
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) readLoop() {
|
|
||||||
for {
|
|
||||||
packet, err := c.conn.Read()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Debugw("Cannot read packet", "error", err)
|
|
||||||
c.Close()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := rpc.ParseProxyResponse(packet)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Debugw("Failed response", "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
return
|
|
||||||
case c.channelRead <- response:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Close() {
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
c.logger.Debugw("Closing connection")
|
|
||||||
|
|
||||||
close(c.channelDone)
|
|
||||||
c.conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Done() bool {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return c.Len() == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Len() int {
|
|
||||||
c.proxyConnsMutex.RLock()
|
|
||||||
defer c.proxyConnsMutex.RUnlock()
|
|
||||||
|
|
||||||
return len(c.proxyConns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Attach(conn *ProxyConn) error {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
return ErrClosed
|
|
||||||
case c.channelConnAttach <- conn:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Detach(connID conntypes.ConnID) {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
case c.channelConnDetach <- connID:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConnection(req *protocol.TelegramRequest) (*connection, error) {
|
|
||||||
conn, err := mtproto.TelegramProtocol(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create a new connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id := rand.Int() // nolint: gosec
|
|
||||||
rv := &connection{
|
|
||||||
conn: conn,
|
|
||||||
id: id,
|
|
||||||
logger: zap.S().Named("hub-connection").With("id", id,
|
|
||||||
"dc", req.ClientProtocol.DC(),
|
|
||||||
"protocol", req.ClientProtocol.ConnectionProtocol()),
|
|
||||||
proxyConns: make(map[string]*ProxyConn),
|
|
||||||
|
|
||||||
channelRead: make(chan *rpc.ProxyResponse, 1),
|
|
||||||
channelDone: make(chan struct{}),
|
|
||||||
channelWrite: make(chan conntypes.Packet),
|
|
||||||
channelConnAttach: make(chan *ProxyConn),
|
|
||||||
channelConnDetach: make(chan conntypes.ConnID),
|
|
||||||
}
|
|
||||||
|
|
||||||
go rv.readLoop()
|
|
||||||
|
|
||||||
go rv.run()
|
|
||||||
|
|
||||||
return rv, nil
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type connectionList struct {
|
|
||||||
connections []*connection
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connectionList) get(conn *ProxyConn) (*connection, error) {
|
|
||||||
if len(c.connections) > 0 && c.connections[0].Len() < config.C.MultiplexPerConnection {
|
|
||||||
if err := c.connections[0].Attach(conn); err == nil {
|
|
||||||
return c.connections[0], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newConn, err := newConnection(conn.req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot allocate a new connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = newConn.Attach(conn); err != nil {
|
|
||||||
newConn.Close()
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("cannot attach to the newly created connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.connections = append(c.connections, newConn)
|
|
||||||
lastIndex := len(c.connections) - 1
|
|
||||||
c.connections[0], c.connections[lastIndex] = c.connections[lastIndex], c.connections[0]
|
|
||||||
|
|
||||||
return newConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connectionList) gc() {
|
|
||||||
prevLen := len(c.connections)
|
|
||||||
if prevLen == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := len(c.connections) - 1; i >= 0; i-- {
|
|
||||||
lastIndex := len(c.connections) - 1
|
|
||||||
|
|
||||||
if c.connections[i].Done() {
|
|
||||||
c.connections[i].Close()
|
|
||||||
|
|
||||||
if len(c.connections)-1 == i {
|
|
||||||
c.connections = c.connections[:lastIndex]
|
|
||||||
} else {
|
|
||||||
c.connections[i], c.connections[lastIndex] = c.connections[lastIndex], c.connections[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if prevLen != len(c.connections) {
|
|
||||||
c.sort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connectionList) sort() {
|
|
||||||
if len(c.connections) > 1 {
|
|
||||||
sort.Slice(c.connections, func(i, j int) bool {
|
|
||||||
return c.connections[i].Len() < c.connections[j].Len()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
type hub struct {
|
|
||||||
muxes map[int32]*mux
|
|
||||||
mutex sync.RWMutex
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hub) Register(req *protocol.TelegramRequest) (*ProxyConn, error) {
|
|
||||||
return h.getMux(req).Get(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hub) getMux(req *protocol.TelegramRequest) *mux {
|
|
||||||
var key int32 = 32767 + int32(req.ClientProtocol.DC()) + 100000*int32(req.ClientProtocol.ConnectionProtocol())
|
|
||||||
|
|
||||||
h.mutex.RLock()
|
|
||||||
m, ok := h.muxes[key]
|
|
||||||
h.mutex.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
h.mutex.Lock()
|
|
||||||
m, ok = h.muxes[key]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
m = newMux(h.ctx)
|
|
||||||
h.muxes[key] = m
|
|
||||||
}
|
|
||||||
|
|
||||||
h.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTimeout = errors.New("timeout")
|
|
||||||
ErrClosed = errors.New("context is closed")
|
|
||||||
|
|
||||||
Hub Interface
|
|
||||||
initOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init(ctx context.Context) {
|
|
||||||
initOnce.Do(func() {
|
|
||||||
Hub = &hub{
|
|
||||||
muxes: make(map[int32]*mux),
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import "github.com/9seconds/mtg/protocol"
|
|
||||||
|
|
||||||
type Interface interface {
|
|
||||||
Register(*protocol.TelegramRequest) (*ProxyConn, error)
|
|
||||||
}
|
|
||||||
-90
@@ -1,90 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const muxGCEvery = time.Minute
|
|
||||||
|
|
||||||
type muxNewRequest struct {
|
|
||||||
req *protocol.TelegramRequest
|
|
||||||
resp chan<- muxNewResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
type muxNewResponse struct {
|
|
||||||
conn *ProxyConn
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type mux struct {
|
|
||||||
connections connectionList
|
|
||||||
clients map[string]*connection
|
|
||||||
ctx context.Context
|
|
||||||
channelClosed chan conntypes.ConnID
|
|
||||||
channelNew chan muxNewRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mux) run() {
|
|
||||||
gcTicker := time.NewTicker(muxGCEvery)
|
|
||||||
defer gcTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-m.ctx.Done():
|
|
||||||
for _, v := range m.clients {
|
|
||||||
v.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
case <-gcTicker.C:
|
|
||||||
m.connections.gc()
|
|
||||||
case req := <-m.channelNew:
|
|
||||||
m.connections.gc()
|
|
||||||
proxyConn := newProxyConn(req.req, m.channelClosed)
|
|
||||||
conn, err := m.connections.get(proxyConn)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
m.clients[string(req.req.ConnID[:])] = conn
|
|
||||||
}
|
|
||||||
|
|
||||||
req.resp <- muxNewResponse{
|
|
||||||
conn: proxyConn,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
close(req.resp)
|
|
||||||
case connID := <-m.channelClosed:
|
|
||||||
if conn, ok := m.clients[string(connID[:])]; ok {
|
|
||||||
conn.Detach(connID)
|
|
||||||
delete(m.clients, string(connID[:]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mux) Get(req *protocol.TelegramRequest) (*ProxyConn, error) {
|
|
||||||
resp := make(chan muxNewResponse)
|
|
||||||
m.channelNew <- muxNewRequest{
|
|
||||||
req: req,
|
|
||||||
resp: resp,
|
|
||||||
}
|
|
||||||
|
|
||||||
rv := <-resp
|
|
||||||
|
|
||||||
return rv.conn, rv.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMux(ctx context.Context) *mux {
|
|
||||||
m := &mux{
|
|
||||||
ctx: ctx,
|
|
||||||
clients: make(map[string]*connection),
|
|
||||||
channelClosed: make(chan conntypes.ConnID, 1),
|
|
||||||
channelNew: make(chan muxNewRequest),
|
|
||||||
}
|
|
||||||
go m.run()
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/mtproto/rpc"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
proxyConnWriteTimeout = 2 * time.Minute
|
|
||||||
proxyConnReadTimeout = 2 * time.Minute
|
|
||||||
|
|
||||||
proxyConnBackpressureAfter = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProxyConn struct {
|
|
||||||
closeOnce sync.Once
|
|
||||||
req *protocol.TelegramRequest
|
|
||||||
channelResponse chan *rpc.ProxyResponse
|
|
||||||
channelClosed chan<- conntypes.ConnID
|
|
||||||
channelWrite chan<- conntypes.Packet
|
|
||||||
channelDone chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) Read() (*rpc.ProxyResponse, error) {
|
|
||||||
timer := time.NewTimer(proxyConnReadTimeout)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
return nil, ErrTimeout
|
|
||||||
case <-p.channelDone:
|
|
||||||
return nil, ErrClosed
|
|
||||||
case packet := <-p.channelResponse:
|
|
||||||
return packet, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) Write(packet conntypes.Packet) error {
|
|
||||||
timer := time.NewTimer(proxyConnWriteTimeout)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
return ErrTimeout
|
|
||||||
case <-p.channelDone:
|
|
||||||
return ErrClosed
|
|
||||||
case p.channelWrite <- packet:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) put(response *rpc.ProxyResponse) {
|
|
||||||
select {
|
|
||||||
case <-p.channelDone:
|
|
||||||
case p.channelResponse <- response:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) Close() {
|
|
||||||
p.closeOnce.Do(func() {
|
|
||||||
close(p.channelDone)
|
|
||||||
go func() {
|
|
||||||
p.channelClosed <- p.req.ConnID
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newProxyConn(req *protocol.TelegramRequest, channelClosed chan<- conntypes.ConnID) *ProxyConn {
|
|
||||||
return &ProxyConn{
|
|
||||||
channelResponse: make(chan *rpc.ProxyResponse, proxyConnBackpressureAfter),
|
|
||||||
channelDone: make(chan struct{}),
|
|
||||||
channelClosed: channelClosed,
|
|
||||||
req: req,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/essentials"
|
||||||
|
"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)
|
||||||
|
|
||||||
|
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) (essentials.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() //nolint: errcheck
|
||||||
|
}()
|
||||||
|
|
||||||
|
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,287 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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/ipblocklist/files"
|
||||||
|
"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"
|
||||||
|
"github.com/yl2chen/cidranger"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
userAgent := "mtg/" + version
|
||||||
|
|
||||||
|
baseDialer, err := network.NewDefaultDialer(tcpTimeout, 0)
|
||||||
|
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.ListConfig,
|
||||||
|
logger mtglib.Logger,
|
||||||
|
ntw mtglib.Network,
|
||||||
|
updateCallback ipblocklist.FireholUpdateCallback,
|
||||||
|
) (mtglib.IPBlocklist, error) {
|
||||||
|
if !conf.Enabled.Get(false) {
|
||||||
|
return ipblocklist.NewNoop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteURLs := []string{}
|
||||||
|
localFiles := []string{}
|
||||||
|
|
||||||
|
for _, v := range conf.URLs {
|
||||||
|
if v.IsRemote() {
|
||||||
|
remoteURLs = append(remoteURLs, v.String())
|
||||||
|
} else {
|
||||||
|
localFiles = append(localFiles, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocklist, err := ipblocklist.NewFirehol(logger.Named("ipblockist"),
|
||||||
|
ntw,
|
||||||
|
conf.DownloadConcurrency.Get(1),
|
||||||
|
remoteURLs,
|
||||||
|
localFiles,
|
||||||
|
updateCallback)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("incorrect parameters for firehol: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go blocklist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach))
|
||||||
|
|
||||||
|
return blocklist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeIPAllowlist(conf config.ListConfig,
|
||||||
|
logger mtglib.Logger,
|
||||||
|
ntw mtglib.Network,
|
||||||
|
updateCallback ipblocklist.FireholUpdateCallback,
|
||||||
|
) (mtglib.IPBlocklist, error) {
|
||||||
|
var (
|
||||||
|
allowlist mtglib.IPBlocklist
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if !conf.Enabled.Get(false) {
|
||||||
|
allowlist, err = ipblocklist.NewFireholFromFiles(
|
||||||
|
logger.Named("ipblocklist"),
|
||||||
|
1,
|
||||||
|
[]files.File{
|
||||||
|
files.NewMem([]*net.IPNet{
|
||||||
|
cidranger.AllIPv4,
|
||||||
|
cidranger.AllIPv6,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
updateCallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
go allowlist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach))
|
||||||
|
} else {
|
||||||
|
allowlist, err = makeIPBlocklist(
|
||||||
|
conf,
|
||||||
|
logger,
|
||||||
|
ntw,
|
||||||
|
updateCallback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build allowlist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) {
|
||||||
|
factories := make([]events.ObserverFactory, 0, 2)
|
||||||
|
|
||||||
|
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 { //nolint: funlen
|
||||||
|
logger := makeLogger(conf)
|
||||||
|
|
||||||
|
logger.BindJSON("configuration", conf.String()).Debug("configuration")
|
||||||
|
|
||||||
|
eventStream, err := makeEventStream(conf, logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build event stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ntw, err := makeNetwork(conf, version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build network: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocklist, err := makeIPBlocklist(
|
||||||
|
conf.Defense.Blocklist,
|
||||||
|
logger.Named("blocklist"),
|
||||||
|
ntw,
|
||||||
|
func(ctx context.Context, size int) {
|
||||||
|
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build ip blocklist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowlist, err := makeIPAllowlist(
|
||||||
|
conf.Defense.Allowlist,
|
||||||
|
logger.Named("allowlist"),
|
||||||
|
ntw,
|
||||||
|
func(ctx context.Context, size int) {
|
||||||
|
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, false))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build ip allowlist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dcOverrides := map[int][]string{}
|
||||||
|
for _, override := range conf.DCOverrides {
|
||||||
|
dcid := override.DC.Get()
|
||||||
|
for _, addr := range override.IPs {
|
||||||
|
dcOverrides[dcid] = append(dcOverrides[dcid], addr.Get(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := mtglib.ProxyOpts{
|
||||||
|
Logger: logger,
|
||||||
|
Network: ntw,
|
||||||
|
AntiReplayCache: makeAntiReplayCache(conf),
|
||||||
|
IPBlocklist: blocklist,
|
||||||
|
IPAllowlist: allowlist,
|
||||||
|
EventStream: eventStream,
|
||||||
|
|
||||||
|
Secret: conf.Secret,
|
||||||
|
DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort),
|
||||||
|
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
|
||||||
|
|
||||||
|
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
|
||||||
|
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
|
||||||
|
DCOverrides: dcOverrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, err := mtglib.NewProxy(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create a proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := utils.NewListener(conf.BindTo.Get(""), 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot start proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := utils.RootContext()
|
||||||
|
|
||||||
|
go proxy.Serve(listener) //nolint: errcheck
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
listener.Close() //nolint: errcheck
|
||||||
|
proxy.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
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='Deprecated and ignored'"` //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='1.1.1.1',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
|
||||||
|
Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //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,funlen
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("incorrect concurrency: %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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Socks5Proxies {
|
||||||
|
proxyURL := config.TypeProxyURL{}
|
||||||
|
|
||||||
|
if err := proxyURL.Set(v); err != nil {
|
||||||
|
return fmt.Errorf("incorrect socks5 proxy URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Network.Proxies = append(conf.Network.Proxies, proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,96 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Optional struct {
|
||||||
|
Enabled TypeBool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListConfig struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"`
|
||||||
|
URLs []TypeBlocklistURI `json:"urls"`
|
||||||
|
UpdateEach TypeDuration `json:"updateEach"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Debug TypeBool `json:"debug"`
|
||||||
|
AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"`
|
||||||
|
Secret mtglib.Secret `json:"secret"`
|
||||||
|
BindTo TypeHostPort `json:"bindTo"`
|
||||||
|
PreferIP TypePreferIP `json:"preferIp"`
|
||||||
|
DomainFrontingPort TypePort `json:"domainFrontingPort"`
|
||||||
|
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
|
||||||
|
Concurrency TypeConcurrency `json:"concurrency"`
|
||||||
|
Defense struct {
|
||||||
|
AntiReplay struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
MaxSize TypeBytes `json:"maxSize"`
|
||||||
|
ErrorRate TypeErrorRate `json:"errorRate"`
|
||||||
|
} `json:"antiReplay"`
|
||||||
|
Blocklist ListConfig `json:"blocklist"`
|
||||||
|
Allowlist ListConfig `json:"allowlist"`
|
||||||
|
} `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 {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
Address TypeHostPort `json:"address"`
|
||||||
|
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||||
|
TagFormat TypeStatsdTagFormat `json:"tagFormat"`
|
||||||
|
} `json:"statsd"`
|
||||||
|
Prometheus struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
BindTo TypeHostPort `json:"bindTo"`
|
||||||
|
HTTPPath TypeHTTPPath `json:"httpPath"`
|
||||||
|
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||||
|
} `json:"prometheus"`
|
||||||
|
} `json:"stats"`
|
||||||
|
DCOverrides []struct {
|
||||||
|
DC TypeDC `json:"dc"`
|
||||||
|
IPs []TypeHostPort `json:"ips"`
|
||||||
|
} `json:"dcOverrides"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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,90 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
Allowlist 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:"allowlist" json:"allowlist,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"`
|
||||||
|
DCOverrides []struct {
|
||||||
|
DC uint `toml:"dc" json:"dc"`
|
||||||
|
IPs []string `toml:"ips" json:"ips"`
|
||||||
|
} `toml:"dc-overrides" json:"dcOverrides,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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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,41 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeDC struct {
|
||||||
|
Value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDC) Set(value string) error {
|
||||||
|
parsed, err := strconv.ParseInt(value, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse dc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed < 0 {
|
||||||
|
parsed = -parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = int(parsed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDC) UnmarshalJSON(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDC) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDC) String() string {
|
||||||
|
return strconv.Itoa(t.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDC) Get() int {
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeDCTestStruct struct {
|
||||||
|
Value config.TypeDC `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeDCTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDCTestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"-1s",
|
||||||
|
"1202002020202",
|
||||||
|
"xxx",
|
||||||
|
"-11111111111111",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
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, &typeDCTestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDCTestSuite) TestUnmarshalOk() {
|
||||||
|
testData := map[int]int{
|
||||||
|
1: 1,
|
||||||
|
-1: 1,
|
||||||
|
203: 203,
|
||||||
|
}
|
||||||
|
|
||||||
|
for value, expected := range testData {
|
||||||
|
data, err := json.Marshal(map[string]int{
|
||||||
|
"value": value,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(strconv.Itoa(value), func(t *testing.T) {
|
||||||
|
testStruct := &typeDCTestStruct{}
|
||||||
|
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
assert.Equal(t, expected, testStruct.Value.Value)
|
||||||
|
assert.Equal(t, expected, testStruct.Value.Get())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDCTestSuite) TestMarshalOk() {
|
||||||
|
testData := map[string]int{
|
||||||
|
"1": 1,
|
||||||
|
"203": 203,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testData {
|
||||||
|
value := k
|
||||||
|
expected := v
|
||||||
|
|
||||||
|
suite.T().Run(value, func(t *testing.T) {
|
||||||
|
testStruct := &typeDCTestStruct{}
|
||||||
|
|
||||||
|
assert.NoError(t, testStruct.Value.Set(value))
|
||||||
|
|
||||||
|
data, err := json.Marshal(testStruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectedJSON, err := json.Marshal(map[string]int{
|
||||||
|
"value": expected,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(expectedJSON), string(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeDC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeDCTestSuite{})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user