mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 19:34:03 +03:00
REPOSITORY / ScuroNeko/mtg
Compare commits
Compare commits
492 Commits
0.10
..
v2.0.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7718f62477 | ||
|
|
df0994dad3 | ||
|
|
dca43853fb | ||
|
|
ea8e31346c | ||
|
|
ed495b3800 | ||
|
|
4025f223c4 | ||
|
|
90e0ce4c48 | ||
|
|
ac0b442b43 | ||
|
|
af7021fac8 | ||
|
|
5468625212 | ||
|
|
345c1bf715 | ||
|
|
52d47a9786 | ||
|
|
2ba79a8508 | ||
|
|
ce7769472d | ||
|
|
eb5eea2625 | ||
|
|
d965dc3c07 | ||
|
|
040b3c371c | ||
|
|
c7ba8e54be | ||
|
|
33b1a5426c | ||
|
|
91197c0334 | ||
|
|
e075169dd4 | ||
|
|
225444b1b3 | ||
|
|
586ac9dcb9 | ||
|
|
c3be5311e8 | ||
|
|
bfeded67ee | ||
|
|
dde0d25b77 | ||
|
|
296a34cd6e | ||
|
|
44cea68e83 | ||
|
|
5d17e264e6 | ||
|
|
7ef1e99f66 | ||
|
|
3acbacbdb3 | ||
|
|
5fd7062972 | ||
|
|
7a6695f6c4 | ||
|
|
8ac786581b | ||
|
|
86a8f69273 | ||
|
|
95b16cd805 | ||
|
|
3996b9a6b8 | ||
|
|
e286dea650 | ||
|
|
5e27e48129 | ||
|
|
95abb54dc8 | ||
|
|
dbd3d3c266 | ||
|
|
ad7acee64d | ||
|
|
9ce6674070 | ||
|
|
25edb60f2c | ||
|
|
b5a8d8b804 | ||
|
|
35f0fca3c5 | ||
|
|
cd46a734ea | ||
|
|
cd92a343aa | ||
|
|
289e8f5945 | ||
|
|
850bed0dfc | ||
|
|
8977d77a13 | ||
|
|
91726d461f | ||
|
|
be1a47287a | ||
|
|
426483d27b | ||
|
|
bc72717582 | ||
|
|
391d901402 | ||
|
|
837d96dc43 | ||
|
|
9125a29e79 | ||
|
|
6449109b2b | ||
|
|
829669eefd | ||
|
|
2d9f2345dc | ||
|
|
6a9069ca72 | ||
|
|
d72ed03a43 | ||
|
|
f0d2cd494a | ||
|
|
b5810c955d | ||
|
|
4ebae693e0 | ||
|
|
1a619e3090 | ||
|
|
8e29ff78ba | ||
|
|
efdac2c0d8 | ||
|
|
e559388802 | ||
|
|
cb691453f0 | ||
|
|
020065f389 | ||
|
|
cf5197cee0 | ||
|
|
720158f15c | ||
|
|
07f00a363e | ||
|
|
d32ab34e60 | ||
|
|
5a52418394 | ||
|
|
cf43ae780b | ||
|
|
2407748ab0 | ||
|
|
a47edf08d0 | ||
|
|
566955b8b7 | ||
|
|
5cc07a54a2 | ||
|
|
ba2143a6ae | ||
|
|
acb02baa2e | ||
|
|
422ed5b071 | ||
|
|
19efcc93fc | ||
|
|
b4c47ff4e1 | ||
|
|
8a783d6b4e | ||
|
|
c5029c706c | ||
|
|
d7e565e0f5 | ||
|
|
93686377d5 | ||
|
|
87de28636a | ||
|
|
7788b169b6 | ||
|
|
6b9601fc2c | ||
|
|
a65cb05d20 | ||
|
|
31866d5f64 | ||
|
|
0db83a0cc8 | ||
|
|
40bb59faf5 | ||
|
|
307910fe4a | ||
|
|
4c2c468289 | ||
|
|
f2f7c2a4de | ||
|
|
8fdd4292b1 | ||
|
|
44d339d777 | ||
|
|
c6223e256d | ||
|
|
7928c2b323 | ||
|
|
b2e7e1a225 | ||
|
|
843fc4b70f | ||
|
|
ed9689d813 | ||
|
|
59ddffa911 | ||
|
|
3af9ede1c1 | ||
|
|
47364e7ba8 | ||
|
|
b6251608d9 | ||
|
|
6dce3944e4 | ||
|
|
112d3a916c | ||
|
|
5d3824704c | ||
|
|
0ff5ca6808 | ||
|
|
8e0436e89a | ||
|
|
646294b6ba | ||
|
|
196ef3d449 | ||
|
|
4f8755879d | ||
|
|
3451832d13 | ||
|
|
dfa53af567 | ||
|
|
27c5646680 | ||
|
|
596fafe30d | ||
|
|
e465817011 | ||
|
|
ef60877cb0 | ||
|
|
8b802819ee | ||
|
|
3c75fcca80 | ||
|
|
1258f07197 | ||
|
|
24137b6ea2 | ||
|
|
7d1d9007af | ||
|
|
ae479adeaa | ||
|
|
6732e80d06 | ||
|
|
2bf291bab5 | ||
|
|
273aa18367 | ||
|
|
b491e03504 | ||
|
|
a7a4da8ee5 | ||
|
|
34ad883559 | ||
|
|
22905b2a25 | ||
|
|
0ff9a58780 | ||
|
|
ed180ecb3d | ||
|
|
f005592872 | ||
|
|
d4cd779c42 | ||
|
|
9e3683ce08 | ||
|
|
d8216f776d | ||
|
|
038b2b200d | ||
|
|
fd8506c82a | ||
|
|
559a1a666b | ||
|
|
4c5f1a5636 | ||
|
|
fbbf9b1634 | ||
|
|
1e428e13fc | ||
|
|
f531d90fc4 | ||
|
|
5f168d1ee4 | ||
|
|
5009859a1e | ||
|
|
432e2970a0 | ||
|
|
6e154b3f44 | ||
|
|
56cf90b13d | ||
|
|
d10729490d | ||
|
|
d459efcf9c | ||
|
|
2eba78b0db | ||
|
|
61450ea3db | ||
|
|
c62afbdfd8 | ||
|
|
828627b0d8 | ||
|
|
c68f2cd50f | ||
|
|
e81c5970d4 | ||
|
|
1a7eee444e | ||
|
|
d9bd07b027 | ||
|
|
3ae69183aa | ||
|
|
6c7edfb7db | ||
|
|
d44474012a | ||
|
|
413cafeeb6 | ||
|
|
c9743b5675 | ||
|
|
072bce2922 | ||
|
|
386522ba22 | ||
|
|
029245cd16 | ||
|
|
b280f47731 | ||
|
|
1617866c24 | ||
|
|
691954bd16 | ||
|
|
9534cb19d0 | ||
|
|
7827d1255a | ||
|
|
d431feb0ba | ||
|
|
3816dbf5b1 | ||
|
|
2918ed11e5 | ||
|
|
701f62ed4c | ||
|
|
299a34252e | ||
|
|
2492a47d0a | ||
|
|
c2b8e88961 | ||
|
|
c9d3020ea6 | ||
|
|
018e5b96f4 | ||
|
|
07985cf418 | ||
|
|
389f6fd4bf | ||
|
|
09c7ce45d2 | ||
|
|
199eed0f18 | ||
|
|
82ec3871a4 | ||
|
|
2748fbd1e5 | ||
|
|
f14f104bea | ||
|
|
b1074193a8 | ||
|
|
78dea9ae3f | ||
|
|
da8dba1585 | ||
|
|
33852ca481 | ||
|
|
eff53694a0 | ||
|
|
74a7c505eb | ||
|
|
9983f7fcba | ||
|
|
a90072260e | ||
|
|
b153240bc3 | ||
|
|
6dfbd26524 | ||
|
|
39e7663e0c | ||
|
|
71f7bf6cad | ||
|
|
fba0e235c1 | ||
|
|
70be70de3c | ||
|
|
b565d83d40 | ||
|
|
ebc459440c | ||
|
|
6e6049a73a | ||
|
|
916714a909 | ||
|
|
f68b195306 | ||
|
|
5f1d1ce883 | ||
|
|
16558a7c55 | ||
|
|
e7958aaf33 | ||
|
|
7d6b661d97 | ||
|
|
28b3cbe91a | ||
|
|
6818364231 | ||
|
|
ea97bf51c8 | ||
|
|
1678ddbd53 | ||
|
|
f81f29cbb4 | ||
|
|
2d9259db48 | ||
|
|
c721c636d9 | ||
|
|
60472072aa | ||
|
|
6721e6fd9f | ||
|
|
47fb5c23cb | ||
|
|
fce6118c71 | ||
|
|
d277a2975a | ||
|
|
c3f21a7b0d | ||
|
|
61a1024264 | ||
|
|
4695a0c433 | ||
|
|
a9d0ca1e90 | ||
|
|
9346c11f37 | ||
|
|
5f00363da5 | ||
|
|
3aec9657d7 | ||
|
|
358d42ec61 | ||
|
|
ac2625209b | ||
|
|
c74e0ee359 | ||
|
|
7c463851b2 | ||
|
|
c9f6b922c3 | ||
|
|
7128e70e50 | ||
|
|
cf9bf56d8e | ||
|
|
76cfbb009a | ||
|
|
cb78169d6a | ||
|
|
d1703873c1 | ||
|
|
ac33abbbb1 | ||
|
|
a6893c8df7 | ||
|
|
7182c7bf65 | ||
|
|
84f4a37d11 | ||
|
|
3cf7f1396b | ||
|
|
835ab94470 | ||
|
|
278753d91e | ||
|
|
b81505ec21 | ||
|
|
0828bd85ad | ||
|
|
96e80fc0d6 | ||
|
|
591500de41 | ||
|
|
07ddfa12f7 | ||
|
|
618d91b15d | ||
|
|
a83d20dd46 | ||
|
|
1e9bbfae55 | ||
|
|
8268620615 | ||
|
|
753639beb8 | ||
|
|
1ee34fd502 | ||
|
|
243a89a68c | ||
|
|
9f20e8749a | ||
|
|
b0d86abc74 | ||
|
|
6c0030d20e | ||
|
|
8733f6d191 | ||
|
|
8906ae0db0 | ||
|
|
2e8c9623de | ||
|
|
0ce325f908 | ||
|
|
b86ad07ba9 | ||
|
|
6253c21615 | ||
|
|
a00408562f | ||
|
|
0d0bdd9fde | ||
|
|
c8da08ecce | ||
|
|
c5f99d17b7 | ||
|
|
c1b862c1e9 |
@@ -0,0 +1,4 @@
|
||||
---
|
||||
|
||||
fixes:
|
||||
- "github.com/9seconds/mtg/v2/::"
|
||||
@@ -0,0 +1,9 @@
|
||||
vendor/
|
||||
tags
|
||||
.github/
|
||||
.bin/
|
||||
*.md
|
||||
mtg
|
||||
.golangci.toml
|
||||
.gitignore
|
||||
run.sh
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- v1
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- released
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
strategy:
|
||||
matrix:
|
||||
go_version:
|
||||
- ^1.16
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go_version }}
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
|
||||
- name: Run tests
|
||||
run: go test -coverprofile=./coverage.txt -covermode=atomic -v ./...
|
||||
|
||||
- name: Collect coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Run linter
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.39.0
|
||||
|
||||
docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
with:
|
||||
images: nineseconds/mtg,ghcr.io/9seconds/mtg
|
||||
tag-semver: "{{version}},{{major}},{{major}}.{{minor}}"
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Setup BuildX
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR.io
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
pull: true
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/buildx-cache
|
||||
cache-to: type=local,dest=/tmp/buildx-cache
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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"
|
||||
|
||||
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
-1
@@ -7,5 +7,6 @@
|
||||
*.out
|
||||
mtg
|
||||
vendor/
|
||||
version.go
|
||||
ccbuilds/
|
||||
.bin/
|
||||
coverage.txt
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[run]
|
||||
concurrency = 4
|
||||
deadline = "2m"
|
||||
tests = true
|
||||
skip-dirs = ["vendor"]
|
||||
|
||||
[output]
|
||||
format = "colored-line-number"
|
||||
|
||||
[linters]
|
||||
enable-all = true
|
||||
disable = ["gochecknoglobals", "gas", "goerr113", "exhaustivestruct"]
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"aggregate": true,
|
||||
"vendor": true
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
---
|
||||
|
||||
language: go
|
||||
sudo: false
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- "1.9.x"
|
||||
- "1.10.x"
|
||||
- master
|
||||
|
||||
script:
|
||||
- make lint
|
||||
- make test
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
+10
-17
@@ -1,7 +1,7 @@
|
||||
###############################################################################
|
||||
# BUILD STAGE
|
||||
|
||||
FROM golang:alpine
|
||||
FROM golang:1.16-alpine AS build
|
||||
|
||||
RUN set -x \
|
||||
&& apk --no-cache --update add \
|
||||
@@ -9,17 +9,13 @@ RUN set -x \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
make \
|
||||
upx \
|
||||
&& update-ca-certificates
|
||||
make
|
||||
|
||||
ADD . /go/src/github.com/9seconds/mtg
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN set -x \
|
||||
&& cd /go/src/github.com/9seconds/mtg \
|
||||
&& make clean \
|
||||
&& make -j 4 static \
|
||||
&& upx --ultra-brute -qq ./mtg
|
||||
&& make -j 4 static
|
||||
|
||||
|
||||
###############################################################################
|
||||
@@ -27,12 +23,9 @@ RUN set -x \
|
||||
|
||||
FROM scratch
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/mtg"]
|
||||
ENV MTG_IP=0.0.0.0 \
|
||||
MTG_PORT=3128 \
|
||||
MTG_STATS_IP=0.0.0.0 \
|
||||
MTG_STATS_PORT=3129
|
||||
EXPOSE 3128 3129
|
||||
ENTRYPOINT ["/mtg"]
|
||||
CMD ["run", "/config.toml"]
|
||||
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=0 /go/src/github.com/9seconds/mtg/mtg /usr/local/bin/mtg
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=build /app/mtg /mtg
|
||||
COPY --from=build /app/example.config.toml /config.toml
|
||||
|
||||
Generated
-114
@@ -1,114 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [
|
||||
".",
|
||||
"parse"
|
||||
]
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/units"
|
||||
packages = ["."]
|
||||
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/beevik/ntp"
|
||||
packages = ["."]
|
||||
revision = "62c80a04de2086884d8296004b6d74ee1846c582"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/juju/errors"
|
||||
packages = ["."]
|
||||
revision = "c7d06af17c68cd34c835053720b21f6549d9b0ee"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"internal/bufferpool",
|
||||
"internal/color",
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"ipv4"
|
||||
]
|
||||
revision = "d0887baf81f4598189d4e12a37c6da86f0bba4d0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alexcesaro/statsd.v2"
|
||||
packages = ["."]
|
||||
revision = "7fea3f0d2fab1ad973e641e51dba45443a311a90"
|
||||
version = "v2.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "7fad0f62feb7737b064d85cc4333a1a3e9298faec2afd864b4404f515fc7f17c"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
version = "2.2.6"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/juju/errors"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/beevik/ntp"
|
||||
version = "0.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/alexcesaro/statsd.v2"
|
||||
version = "2.0.0"
|
||||
@@ -1,80 +1,107 @@
|
||||
ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
IMAGE_NAME := mtg
|
||||
APP_NAME := $(IMAGE_NAME)
|
||||
GOMETALINTER := gometalinter
|
||||
|
||||
VENDOR_FILES := $(shell find "$(ROOT_DIR)/vendor" 2>/dev/null || echo -n "vendor")
|
||||
CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}")
|
||||
APP_DEPS := version.go $(VENDOR_FILES)
|
||||
|
||||
COMMON_BUILD_FLAGS := -ldflags="-s -w"
|
||||
GOLANGCI_LINT_VERSION := v1.39.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
VERSION_GO := $(shell go version)
|
||||
VERSION_DATE := $(shell date -Ru)
|
||||
VERSION_TAG := $(shell git describe --tags --always)
|
||||
COMMON_BUILD_FLAGS := -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
|
||||
|
||||
$(APP_NAME): $(APP_DEPS)
|
||||
@go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
static-$(APP_NAME): $(APP_DEPS)
|
||||
@env CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
$(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
|
||||
$(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
|
||||
$(APP_NAME)-%: $(APP_DEPS) ccbuilds
|
||||
@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
|
||||
go build \
|
||||
$(COMMON_BUILD_FLAGS) \
|
||||
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
|
||||
|
||||
ccbuilds:
|
||||
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
|
||||
|
||||
version.go:
|
||||
@go generate main.go
|
||||
|
||||
vendor: Gopkg.lock Gopkg.toml install-cli
|
||||
@dep ensure
|
||||
GOBIN := $(ROOT_DIR)/.bin
|
||||
GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
.PHONY: all
|
||||
all: $(APP_NAME)
|
||||
all: build
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
$(APP_NAME): build
|
||||
|
||||
.PHONY: static
|
||||
static: static-$(APP_NAME)
|
||||
static:
|
||||
@env CGO_ENABLED=0 GOOS=linux go build \
|
||||
$(COMMON_BUILD_FLAGS) \
|
||||
-tags netgo \
|
||||
-a \
|
||||
-o "$(APP_NAME)"
|
||||
|
||||
$(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
|
||||
@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
|
||||
go build \
|
||||
$(COMMON_BUILD_FLAGS) \
|
||||
-tags netgo \
|
||||
-a \
|
||||
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
|
||||
|
||||
.PHONY: ccbuilds
|
||||
ccbuilds:
|
||||
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
|
||||
|
||||
vendor: go.mod go.sum
|
||||
@$(MOD_ON) go mod vendor
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@$(GOTOOL) gofumpt -w -s -extra "$(ROOT_DIR)"
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@go test -v ./...
|
||||
|
||||
.PHONY: citest
|
||||
citest:
|
||||
@go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./...
|
||||
|
||||
.PHONY: crosscompile
|
||||
crosscompile: $(CC_BINARIES)
|
||||
|
||||
.PHONY: crosscompile-dir
|
||||
crosscompile-dir:
|
||||
@rm -rf "$(CC_DIR)" && mkdir -p "$(CC_DIR)"
|
||||
|
||||
.PHONY: test
|
||||
test: vendor install-cli version.go
|
||||
@go test -v ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint: vendor install-cli version.go
|
||||
@$(GOMETALINTER) --deadline=2m ./...
|
||||
|
||||
.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: lint
|
||||
lint:
|
||||
@$(GOTOOL) golangci-lint run
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
|
||||
|
||||
.PHONY: install-cli
|
||||
install-cli: install-dep install-lint
|
||||
.PHONY: doc
|
||||
doc:
|
||||
@$(GOTOOL) godoc -http 0.0.0.0:10000
|
||||
|
||||
.PHONY: install-dep
|
||||
install-dep:
|
||||
@go get github.com/golang/dep/cmd/dep
|
||||
.PHONY: install-tools
|
||||
install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt
|
||||
|
||||
.PHONY: install-lint
|
||||
install-lint:
|
||||
@go get github.com/alecthomas/gometalinter && \
|
||||
$(GOMETALINTER) --install >/dev/null
|
||||
.PHONY: install-tools-lint
|
||||
install-tools-lint:
|
||||
@mkdir -p "$(GOBIN)" || true && \
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
|
||||
| bash -s -- -b "$(GOBIN)" "$(GOLANGCI_LINT_VERSION)"
|
||||
|
||||
.PHONY: install-tools-godoc
|
||||
install-tools-godoc:
|
||||
@mkdir -p "$(GOBIN)" || true && \
|
||||
$(GOTOOL) go get -u golang.org/x/tools/cmd/godoc
|
||||
|
||||
.PHONY: install-tools-gofumpt
|
||||
install-tools-gofumpt:
|
||||
@mkdir -p "$(GOBIN)" || true && \
|
||||
$(GOTOOL) go get -u mvdan.cc/gofumpt
|
||||
|
||||
.PHONY: update-deps
|
||||
upgrade-deps:
|
||||
$go get -u && go mod tidy
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# mtg
|
||||
|
||||
Bullshit-free MTPROTO proxy for Telegram
|
||||
Highly-opionated (ex-bullshit-free) MTPROTO proxy for
|
||||
[Telegram](https://telegram.org/).
|
||||
|
||||
[](https://travis-ci.org/9seconds/mtg)
|
||||
[](https://hub.docker.com/r/nineseconds/mtg/)
|
||||
[](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
|
||||
[](https://codecov.io/gh/9seconds/mtg)
|
||||
[](https://pkg.go.dev/github.com/9seconds/mtg/v2)
|
||||
|
||||
**If you use v1.0 or upgrade broke you proxy, please read the chapter
|
||||
[Version 2](#version-2)**
|
||||
|
||||
# Rationale
|
||||
## Rationale
|
||||
|
||||
There are several available proxies for Telegram MTPROTO available. Here
|
||||
are the most notable:
|
||||
@@ -15,183 +19,322 @@ are the most notable:
|
||||
* [Python](https://github.com/alexbers/mtprotoproxy)
|
||||
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
||||
|
||||
Almost all of them follow the way how official proxy was build. This
|
||||
includes support of multiple secrets, support of promoted channels etc.
|
||||
You can use any of these. They work great and all implementations have
|
||||
feature parity now. This includes support of adtag, replay attack
|
||||
protection, domain fronting, faketls, and so on. mtg has a similar
|
||||
goal: to give a possibility to connect to Telegram in a restricted,
|
||||
censored environment. But it does it slightly differently in details
|
||||
that probably matter.
|
||||
|
||||
mtg is an implementation in golang which is intended to be:
|
||||
* **Resource-efficient**
|
||||
|
||||
It has to be resource-efficient. It does not mean that you will see
|
||||
the smallest memory usage. It means that it will try to use allocated
|
||||
resources in zero-waste mode, reusing as much memory as possible and
|
||||
so on.
|
||||
|
||||
* **Lightweight**
|
||||
It has to consume as less resources as possible but not by losing
|
||||
maintainability.
|
||||
* **Easily deployable**
|
||||
|
||||
I strongly believe that Telegram proxies should follow the way of
|
||||
ShadowSocks: promoted channels is a strange way of doing business
|
||||
I suppose. I think the only viable way is to have a proxy with
|
||||
minimum configuration which should work everywhere.
|
||||
* **Single secret**
|
||||
I think that multiple secrets solves no problems and just complexify
|
||||
software. I also believe that in case of throwout proxies, this feature
|
||||
is useless luxury.
|
||||
* **Minimum docker image size**
|
||||
Official image is less than 2.5 megabytes. Literally.
|
||||
[ShadowSocks](https://shadowsocks.org): promoted channels is a strange
|
||||
way of doing business I suppose. I think the only viable way is to
|
||||
have a proxy that can be restored anywhere easily.
|
||||
|
||||
* **A single secret**
|
||||
|
||||
I think that multiple secrets solve no problems and just complex
|
||||
software. I also believe that in the case of throwout proxies, this
|
||||
the feature is a useless luxury.
|
||||
|
||||
* **No adtag support**
|
||||
|
||||
Please read [Version 2](#version-2) chapter.
|
||||
|
||||
* **No management WebUI**
|
||||
This is an implementation of simple lightweight proxy. I won't do that.
|
||||
|
||||
This proxy supports 2 modes of work: direct connection to Telegram and
|
||||
promoted channel mode. If you do not need promoted channels, I would
|
||||
recommend you to go with direct mode: this is way more robust.
|
||||
This is an implementation of a simple lightweight proxy. I won't do that.
|
||||
|
||||
To run proxy in direct mode, all you need to do is just provide a
|
||||
secret. If you do not provide ADTag as a second parameter, promoted
|
||||
channels mode won't be activated.
|
||||
* **Proxy chaining**
|
||||
|
||||
To get promoted channel, please contact
|
||||
[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
|
||||
second parameter.
|
||||
mtg has the support of [SOCKS5](https://en.wikipedia.org/wiki/SOCKS)
|
||||
proxies. So, in theory, you can run this proxy as a frontend
|
||||
and route traffic via [v2ray](https://www.v2ray.com/),
|
||||
[Gost](https://docs.ginuerzh.xyz/gost/),
|
||||
[Trojan](https://trojan-gfw.github.io/trojan/), or any other project
|
||||
you like.
|
||||
|
||||
* **Native blocklist support**
|
||||
|
||||
# Source code organization
|
||||
Previously, this was delegated to the [FireHOL](https://firehol.org/)
|
||||
project or similar ones which track attacks and publish a list of
|
||||
potentially dangerous IPs. mtg has native support of such blocklists.
|
||||
|
||||
There are 2 main branches:
|
||||
* **Can be used as a library**
|
||||
|
||||
1. `master` branch contains potentially unstable features
|
||||
2. `stable` branch contains stable version. Usually you want to use this branch.
|
||||
mtg v2 was redesigned in a way so it can be embedded into your
|
||||
software (written in Golang) with a minimum effort + you can replace
|
||||
some parts with those you want.
|
||||
|
||||
# How to build
|
||||
### Version 2
|
||||
|
||||
If you use version 1.x before, you are probably noticed some major
|
||||
backward non-compatible details:
|
||||
|
||||
1. Configuration file
|
||||
2. Removed support of adtag
|
||||
|
||||
For the configuration file, please check out the full example in this
|
||||
repository. It has a lot of comments and most of the options are
|
||||
optional. We do have only `secret` and `bind-to` sections mandatory.
|
||||
Other sections in the example configuration file are filled with default
|
||||
values.
|
||||
|
||||
Adtag support was removed completely. This was done to debloat mtg and
|
||||
keep it simple and obvious. Hopefully, this goal is achieved and the
|
||||
source code is clean and straightforward enough.
|
||||
|
||||
I always was quite skeptical about adtag. In my POV, a proxy as a fat
|
||||
big connectivity point for hundreds of clients is an illusion. If you
|
||||
work in a censored environment, the first thing that authority does is
|
||||
IP blocking. For us, it means, those big proxies that can benefit from
|
||||
having a pinned channel are going to be blocked in a minute.
|
||||
|
||||
Proxy has to be intimate. It has to be shared within a small group as
|
||||
a family or maybe your college friends. It has to have a small number
|
||||
of connections and never publicly announced its presence. It has to fly
|
||||
under the radar. If the proxy is detected, you need to be able to give
|
||||
a rebirth on a new IP address as soon as possible. I do no think that
|
||||
having some special channel for such a use case makes any sense.
|
||||
|
||||
But other details like replay attack protection, domain fronting,
|
||||
accurate FakeTLS implementation, IP blacklisting, and proxy
|
||||
chaining matter here. If you work in censored perimeter like
|
||||
[GFW](https://en.wikipedia.org/wiki/Great_Firewall)-protected
|
||||
country, you probably want to have an MTPROTO proxy as
|
||||
a frontend that transports traffic via cloaked tunnels
|
||||
made by [Trojan](https://trojan-gfw.github.io/trojan/),
|
||||
[Shadowsocks](https://shadowsocks.org), [v2ray](https://www.v2ray.com/),
|
||||
or [Gost](https://docs.ginuerzh.xyz/gost/). That's why you have to have
|
||||
the support of chaining as a first-class citizen.
|
||||
|
||||
Yes, this is possible and doable with optional adtag support. But the
|
||||
truth is that the MTPROTO proxy for Telegram is just a thing that either
|
||||
work as a normal client (direct mode) or doing some RPC calls in [TL
|
||||
language](https://core.telegram.org/mtproto/TL) (adtag support). I
|
||||
understand the intention of the developers and I understand that they
|
||||
were under high pressure fighting with [RKN](https://rkn.gov.ru/) and
|
||||
doing TON after that. Nothing is ideal. But for the proxy, it means that
|
||||
source code is full of complex non-trivial code which is required only
|
||||
to support a feature that we barely need.
|
||||
|
||||
So, to have a reasonable MTPROTO proxy, adtag support was removed. This
|
||||
is a rare chance in my career where software v2 debloats a previous
|
||||
version. It feels so good :)
|
||||
|
||||
### Version 1 and 2
|
||||
|
||||
I do continue to support both versions 1 and 2. But in a different mode.
|
||||
|
||||
Version 1 is now officially in maintenance mode. It means that I won't
|
||||
make any new features or improvements there. You can consider a feature
|
||||
freeze there. No bugs are going to be fixed there except for critical
|
||||
ones. PRs are welcome though. The goal is to keep it working. It will
|
||||
get some periodical updates like updates to the new Golang version of
|
||||
dependencies version bump, but that's mostly it.
|
||||
|
||||
**If you want to have mtg with _adtag support_, please use version 1**.
|
||||
|
||||
Version 2 is going to have all my love, active support, bug fixing, etc.
|
||||
It is under active development and maintenance.
|
||||
|
||||
This project has several main branches
|
||||
|
||||
1. [`master`](https://github.com/9seconds/mtg/tree/master) branch
|
||||
contains a bleeding edge. It may potentially have some features
|
||||
which will break your source code.
|
||||
2. [`stable`](https://github.com/9seconds/mtg/tree/stable) branch contains
|
||||
dumps of a master branch when we consider it 'stable'. This is a
|
||||
branch you probably want to pick.
|
||||
3. [`v2`](https://github.com/9seconds/mtg/tree/v2) has a development
|
||||
of the v2.x version. In theory, it is the same as `master` but this
|
||||
will change when we have v3.x.
|
||||
4. [`v1`](https://github.com/9seconds/mtg/tree/v1) has a version 1.x.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Download a tool
|
||||
|
||||
#### Download binaries
|
||||
|
||||
Binaries can be downloaded from the release page. Also, you can download
|
||||
docker image.
|
||||
|
||||
For the current version, please download like
|
||||
|
||||
```console
|
||||
$ make
|
||||
docker pull nineseconds/mtg:2
|
||||
```
|
||||
|
||||
If you want to build for another platform:
|
||||
For version 1:
|
||||
|
||||
```console
|
||||
$ make crosscompile
|
||||
docker pull nineseconds/mtg:1
|
||||
```
|
||||
|
||||
If you want to build Docker image (called `mtg`):
|
||||
You may also check both [Docker
|
||||
Hub](https://hub.docker.com/r/nineseconds/mtg/tags) and [Github
|
||||
Registry](https://github.com/users/9seconds/packages/container/package/mtg).
|
||||
Please do not choose `latest` or `stable` if you want to avoid
|
||||
surprises. Always choose some version tag.
|
||||
|
||||
Also, if you have `go` installed, you can always download this tool with `go get`:
|
||||
|
||||
```console
|
||||
$ make docker
|
||||
go get github.com/9seconds/mtg/v2
|
||||
```
|
||||
|
||||
# Docker image
|
||||
|
||||
Docker follows the same policy as the source code organization:
|
||||
|
||||
- `latest` mirrors the master branch
|
||||
- `stable` mirrors the stable branch
|
||||
- tags are for tagged releases
|
||||
#### Build from sources
|
||||
|
||||
```console
|
||||
$ docker pull nineseconds/mtg:latest
|
||||
git clone https://github.com:9seconds/mtg.git
|
||||
cd mtg
|
||||
make static
|
||||
```
|
||||
|
||||
or for the docker image:
|
||||
|
||||
```console
|
||||
$ docker pull nineseconds/mtg:stable
|
||||
make docker
|
||||
```
|
||||
|
||||
```console
|
||||
$ docker pull nineseconds/mtg:0.10
|
||||
```
|
||||
### Generate secret
|
||||
|
||||
# Configuration
|
||||
|
||||
Basically, to run this tool you need to configure as less as possible.
|
||||
|
||||
First, you need to generate a secret:
|
||||
If you already have a secret in Base64 format or that, which starts with `ee`,
|
||||
you can skip this chapter. Otherwise:
|
||||
|
||||
```console
|
||||
$ openssl rand -hex 16
|
||||
$ mtg generate-secret google.com
|
||||
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```console
|
||||
$ head -c 512 /dev/urandom | md5sum | cut -f 1 -d ' '
|
||||
$ mtg generate-secret --hex google.com
|
||||
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||
```
|
||||
|
||||
## Secure mode
|
||||
This secret is a keystone for a proxy and your password for a client.
|
||||
You need to keep it secured.
|
||||
|
||||
If you want to support new secure mode, please prepend `dd` to the
|
||||
secret. For example, secret `cf18fa8ea0267057e2c61a5f7322a8e7` should
|
||||
be `ddcf18fa8ea0267057e2c61a5f7322a8e7`. But pay attention that some
|
||||
old clients won't support this mode. If this is not your case, I would
|
||||
suggest to go with this mode.
|
||||
We recommend choosing a hostname wisely. Here we have a _google.com_
|
||||
but in reality, all providers can easily detect that this is not a
|
||||
Google. Google has a list of networks it officially uses and your IP
|
||||
address won't probably belong to it. It is a great idea to hide behind
|
||||
some domain that has some relation to this IP address.
|
||||
|
||||
Oneliners to generate such secrets:
|
||||
For example, you've bought a VPS from [Digital
|
||||
Ocean](https://www.digitalocean.com/). Then it might be a good idea to
|
||||
generate a secret for _digitalocean.com_ then.
|
||||
|
||||
### 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
|
||||
$ echo dd$(openssl rand -hex 16)
|
||||
$ cat /etc/systemd/system/mtg.service
|
||||
[Unit]
|
||||
Description=mtg
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
$ sudo systemctl daemon-reload
|
||||
$ sudo systemctl enable mtg
|
||||
$ sudo systemctl start mtg
|
||||
```
|
||||
|
||||
or
|
||||
or you can run a docker image
|
||||
|
||||
```console
|
||||
$ echo dd$(head -c 512 /dev/urandom | md5sum | cut -f 1 -d ' ')
|
||||
docker run -d -v /etc/mtg.toml:/config.toml -p 443:3128 --restart=unless-stopped nineseconds/mtg:2
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# How to run the tool
|
||||
### Access a proxy
|
||||
|
||||
Now run the tool:
|
||||
Now you can generate some useful links:
|
||||
|
||||
```console
|
||||
$ mtg <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:
|
||||
## Metrics
|
||||
|
||||
```console
|
||||
$ mtg <secret> <adtag>
|
||||
```
|
||||
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.
|
||||
|
||||
This tool will listen on port 3128 by default with the given secret.
|
||||
Here goes a list of metrics with their types but without a prefix.
|
||||
|
||||
# One-line runner
|
||||
| Name | Type | Tags | Description |
|
||||
|-----------------------------|---------|----------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| client_connections | gauge | `ip_family` | Count of processing client connections. |
|
||||
| telegram_connections | gauge | `telegram_ip`, `dc` | Count of connections to Telegram servers. |
|
||||
| domain_fronting_connections | gauge | `ip_family` | Count of connections to fronting domain. |
|
||||
| telegram_traffic | counter | `telegram_ip`, `dc`, `direction` | Count of bytes, transmitted to/from Telegram. |
|
||||
| domain_fronting_traffic | counter | `direction` | Count of bytes, transmitted to/from fronting domain. |
|
||||
| domain_fronting | counter | – | Count of domain fronting events. |
|
||||
| concurrency_limited | counter | – | Count of events, when client connection was rejected due to concurrency limit. |
|
||||
| ip_blocklisted | counter | – | Count of events when client connection was rejected because IP was found in the blacklist. |
|
||||
| replay_attacks | counter | – | Count of detected replay attacks. |
|
||||
|
||||
```console
|
||||
$ docker run --name mtg --restart=unless-stopped -p 3128:3128 -p 3129:3129 -d nineseconds/mtg:stable $(openssl rand -hex 16)
|
||||
```
|
||||
Tag meaning:
|
||||
|
||||
or in secret mode:
|
||||
|
||||
```console
|
||||
$ docker run --name mtg --restart=unless-stopped -p 3128:3128 -p 3129:3129 -d nineseconds/mtg:stable dd$(openssl rand -hex 16)
|
||||
```
|
||||
|
||||
You will have this tool up and running on port 3128. Now curl
|
||||
`localhost:3129` to get `tg://` links or do `docker logs mtg`. Also,
|
||||
port 3129 will show you some statistics if you are interested in.
|
||||
|
||||
Also, you can use [run-mtg.sh](https://github.com/9seconds/mtg/blob/master/run-mtg.sh) script
|
||||
|
||||
|
||||
# statsd integration
|
||||
|
||||
mtg provides an integration with statsd, you can enable it with command
|
||||
line interface. To enable it, you have to provide IP address of statsd
|
||||
service.
|
||||
|
||||
Out of the box, mtg supports 2 additional dialects: [InfluxDB](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/)
|
||||
and [Datadog](https://docs.datadoghq.com/developers/dogstatsd/).
|
||||
|
||||
All metrics are gauges. Here is the list of metrics and their meaning:
|
||||
|
||||
| Metric name | Unit | Description |
|
||||
|---------------------------------|---------|-----------------------------------------------------------|
|
||||
| `connections.abridged.ipv4` | number | The number of active abridged IPv4 connections |
|
||||
| `connections.abridged.ipv6` | number | The number of active abridged IPv6 connections |
|
||||
| `connections.intermediate.ipv4` | number | The number of active intermediate IPv4 connections |
|
||||
| `connections.intermediate.ipv6` | number | The number of active intermediate IPv6 connections |
|
||||
| `connections.secure.ipv4` | number | The number of active secure intermediate IPv4 connections |
|
||||
| `connections.secure.ipv6` | number | The number of active secure intermediate IPv6 connections |
|
||||
| `crashes` | number | An amount of crashes in client handlers |
|
||||
| `traffic.ingress` | bytes | Ingress traffic from the start of application (incoming) |
|
||||
| `traffic.egress` | bytes | Egress traffic from the start of application (outgoing) |
|
||||
| `speed.ingress` | bytes/s | Ingress bandwidth of the latest second (incoming traffic) |
|
||||
| `speed.egress` | bytes/s | Egress bandwidth of the latest second (outgoing traffic) |
|
||||
|
||||
All metrics are prefixed with given prefix. Default prefix is `mtg`.
|
||||
With such prefix metric name `traffic.ingress`, for example, would be
|
||||
`mtg.traffic.ingress`.
|
||||
| Name | Values | Description |
|
||||
|-------------|------------------------|-----------------------------------------------|
|
||||
| ip_family | ipv4, ipv6 | A version of the IP protocol. |
|
||||
| dc | | A number of the Telegram DC for a connection. |
|
||||
| telegram_ip | | IP address of the Telegram server. |
|
||||
| direction | to_client, from_client | A direction of the traffic flow. |
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We support 2 tracks: 2.x and 1.x
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x | :white_check_mark: |
|
||||
| 1.x | :interrobang: |
|
||||
| < 1.0 | :x: |
|
||||
|
||||
1.x has adtag support. We do not plan any active development there but we guarantee:
|
||||
|
||||
1. Regular dependency updates
|
||||
2. Updates for Golang versions
|
||||
3. Security vulnerability fixes
|
||||
4. Merging small PRs
|
||||
|
||||
2.x is in active development and have a full support.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you have found a vulnerability, please report about it to nineseconds@yandex.ru, telegram @9seconds or here in issues
|
||||
@@ -0,0 +1,17 @@
|
||||
// Antireplay package has cache implementations that are effective
|
||||
// against replay attacks.
|
||||
//
|
||||
// To understand more about replay attacks, please read documentation
|
||||
// for mtglib.AntiReplayCache interface. This package has a list of some
|
||||
// implementations of this interface.
|
||||
package antireplay
|
||||
|
||||
const (
|
||||
// DefaultStableBloomFilterMaxSize is a recommended byte size for a
|
||||
// stable bloom filter.
|
||||
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
|
||||
|
||||
// DefaultStableBloomFilterErrorRate is a recommended default error
|
||||
// rate for a stable bloom filter.
|
||||
DefaultStableBloomFilterErrorRate = 0.001
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package antireplay
|
||||
|
||||
import "github.com/9seconds/mtg/v2/mtglib"
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) SeenBefore(_ []byte) bool { return false }
|
||||
|
||||
// NewNoop returns an implementation that does nothing. A corresponding
|
||||
// method always returns false, so this cache accepts everything you
|
||||
// pass to it.
|
||||
func NewNoop() mtglib.AntiReplayCache {
|
||||
return noop{}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package antireplay_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestOp() {
|
||||
filter := antireplay.NewNoop()
|
||||
|
||||
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package antireplay
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/OneOfOne/xxhash"
|
||||
boom "github.com/tylertreat/BoomFilters"
|
||||
)
|
||||
|
||||
type stableBloomFilter struct {
|
||||
filter boom.StableBloomFilter
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (s *stableBloomFilter) SeenBefore(digest []byte) bool {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
return s.filter.TestAndAdd(digest)
|
||||
}
|
||||
|
||||
// NewStableBloomFilter returns an implementation of AntiReplayCache
|
||||
// based on stable bloom filter.
|
||||
//
|
||||
// http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf
|
||||
//
|
||||
// The basic idea of a stable bloom filter is quite simple: each time
|
||||
// when you set a new element, you randomly reset P elements. There is a
|
||||
// hardcore math which proves that if you choose this P correctly, you
|
||||
// can maintain the same error rate for a stream of elements.
|
||||
//
|
||||
// byteSize is the number of bytes you want to give to a bloom filter.
|
||||
// errorRate is desired false-positive error rate. If you want to use
|
||||
// default values, please pass 0 for byteSize and <0 for errorRate.
|
||||
func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache {
|
||||
if byteSize == 0 {
|
||||
byteSize = DefaultStableBloomFilterMaxSize
|
||||
}
|
||||
|
||||
if errorRate < 0 {
|
||||
errorRate = DefaultStableBloomFilterErrorRate
|
||||
}
|
||||
|
||||
sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate) // nolint: gomnd
|
||||
sf.SetHash(xxhash.New64())
|
||||
|
||||
return &stableBloomFilter{
|
||||
filter: *sf,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package antireplay_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type StableBloomFilterTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *StableBloomFilterTestSuite) TestOp() {
|
||||
filter := antireplay.NewStableBloomFilter(500, 0.001)
|
||||
|
||||
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
suite.True(filter.SeenBefore([]byte{1, 2, 3}))
|
||||
suite.True(filter.SeenBefore([]byte{4, 5, 6}))
|
||||
}
|
||||
|
||||
func TestStableBloomFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &StableBloomFilterTestSuite{})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/wrappers"
|
||||
)
|
||||
|
||||
// Init defines common method for initializing client connections.
|
||||
type Init func(net.Conn, string, *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error)
|
||||
@@ -1,55 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/juju/errors"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/obfuscated2"
|
||||
"github.com/9seconds/mtg/wrappers"
|
||||
)
|
||||
|
||||
const (
|
||||
handshakeTimeout = 10 * time.Second
|
||||
readBufferSize = 64 * 1024
|
||||
writeBufferSize = 64 * 1024
|
||||
)
|
||||
|
||||
// DirectInit initializes client connection for proxy which connects to
|
||||
// Telegram directly.
|
||||
func DirectInit(socket net.Conn, connID string, conf *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) {
|
||||
tcpSocket := socket.(*net.TCPConn)
|
||||
if err := tcpSocket.SetNoDelay(false); err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot disable NO_DELAY to client socket")
|
||||
}
|
||||
if err := tcpSocket.SetReadBuffer(readBufferSize); err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot set read buffer size of client socket")
|
||||
}
|
||||
if err := tcpSocket.SetWriteBuffer(writeBufferSize); err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot set write buffer size of client socket")
|
||||
}
|
||||
|
||||
socket.SetReadDeadline(time.Now().Add(handshakeTimeout)) // nolint: errcheck
|
||||
frame, err := obfuscated2.ExtractFrame(socket)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot extract frame")
|
||||
}
|
||||
socket.SetReadDeadline(time.Time{}) // nolint: errcheck
|
||||
|
||||
conn := wrappers.NewConn(socket, connID, wrappers.ConnPurposeClient, conf.PublicIPv4, conf.PublicIPv6)
|
||||
obfs2, connOpts, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Annotate(err, "Cannot parse obfuscated frame")
|
||||
}
|
||||
connOpts.ConnectionProto = mtproto.ConnectionProtocolAny
|
||||
connOpts.ClientAddr = conn.RemoteAddr()
|
||||
|
||||
conn = wrappers.NewStreamCipher(conn, obfs2.Encryptor, obfs2.Decryptor)
|
||||
|
||||
conn.Logger().Infow("Client connection initialized")
|
||||
|
||||
return conn, connOpts, nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/wrappers"
|
||||
)
|
||||
|
||||
// MiddleInit initializes client connection for proxy which has to
|
||||
// support promoted channels, connect to Telegram middle proxies etc.
|
||||
func MiddleInit(socket net.Conn, connID string, conf *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) {
|
||||
conn, opts, err := DirectInit(socket, connID, conf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
connStream := conn.(wrappers.StreamReadWriteCloser)
|
||||
|
||||
var newConn wrappers.PacketReadWriteCloser
|
||||
switch opts.ConnectionType {
|
||||
case mtproto.ConnectionTypeAbridged:
|
||||
newConn = wrappers.NewMTProtoAbridged(connStream, opts)
|
||||
case mtproto.ConnectionTypeIntermediate:
|
||||
newConn = wrappers.NewMTProtoIntermediate(connStream, opts)
|
||||
case mtproto.ConnectionTypeSecure:
|
||||
newConn = wrappers.NewMTProtoIntermediateSecure(connStream, opts)
|
||||
default:
|
||||
panic("Unknown connection type")
|
||||
}
|
||||
|
||||
opts.ConnectionProto = mtproto.ConnectionProtocolIPv4
|
||||
if socket.LocalAddr().(*net.TCPAddr).IP.To4() == nil {
|
||||
opts.ConnectionProto = mtproto.ConnectionProtocolIPv6
|
||||
}
|
||||
|
||||
return newConn, opts, err
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/juju/errors"
|
||||
statsd "gopkg.in/alexcesaro/statsd.v2"
|
||||
)
|
||||
|
||||
// Buffer sizes define internal socket buffer sizes.
|
||||
const (
|
||||
BufferWriteSize = 32 * 1024
|
||||
BufferReadSize = 32 * 1024
|
||||
)
|
||||
|
||||
// Config represents common configuration of mtg.
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Verbose bool
|
||||
SecureMode bool
|
||||
|
||||
BindPort uint16
|
||||
PublicIPv4Port uint16
|
||||
PublicIPv6Port uint16
|
||||
StatsPort uint16
|
||||
|
||||
BindIP net.IP
|
||||
PublicIPv4 net.IP
|
||||
PublicIPv6 net.IP
|
||||
StatsIP net.IP
|
||||
|
||||
StatsD struct {
|
||||
Addr net.Addr
|
||||
Prefix string
|
||||
Tags map[string]string
|
||||
TagsFormat statsd.TagFormat
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
Secret []byte
|
||||
AdTag []byte
|
||||
}
|
||||
|
||||
// URLs contains links to the proxy (tg://, t.me) and their QR codes.
|
||||
type URLs struct {
|
||||
TG string `json:"tg_url"`
|
||||
TMe string `json:"tme_url"`
|
||||
TGQRCode string `json:"tg_qrcode"`
|
||||
TMeQRCode string `json:"tme_qrcode"`
|
||||
}
|
||||
|
||||
// IPURLs contains links to both ipv4 and ipv6 of the proxy.
|
||||
type IPURLs struct {
|
||||
IPv4 URLs `json:"ipv4"`
|
||||
IPv6 URLs `json:"ipv6"`
|
||||
BotSecret string `json:"secret_for_mtproxybot"`
|
||||
}
|
||||
|
||||
// BindAddr returns connection for this server to bind to.
|
||||
func (c *Config) BindAddr() string {
|
||||
return getAddr(c.BindIP, c.BindPort)
|
||||
}
|
||||
|
||||
// StatAddr returns connection string to the stats API.
|
||||
func (c *Config) StatAddr() string {
|
||||
return getAddr(c.StatsIP, c.StatsPort)
|
||||
}
|
||||
|
||||
// UseMiddleProxy defines if this proxy has to connect middle proxies
|
||||
// which supports promoted channels or directly access Telegram.
|
||||
func (c *Config) UseMiddleProxy() bool {
|
||||
return len(c.AdTag) > 0
|
||||
}
|
||||
|
||||
// BotSecretString returns secret string which should work with MTProxybot.
|
||||
func (c *Config) BotSecretString() string {
|
||||
return hex.EncodeToString(c.Secret)
|
||||
}
|
||||
|
||||
// SecretString returns a secret in a form entered on the start of the
|
||||
// application.
|
||||
func (c *Config) SecretString() string {
|
||||
secret := c.BotSecretString()
|
||||
if c.SecureMode {
|
||||
return "dd" + secret
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
// GetURLs returns configured IPURLs instance with links to this server.
|
||||
func (c *Config) GetURLs() IPURLs {
|
||||
urls := IPURLs{}
|
||||
secret := c.SecretString()
|
||||
if c.PublicIPv4 != nil {
|
||||
urls.IPv4 = getURLs(c.PublicIPv4, c.PublicIPv4Port, secret)
|
||||
}
|
||||
if c.PublicIPv6 != nil {
|
||||
urls.IPv6 = getURLs(c.PublicIPv6, c.PublicIPv6Port, secret)
|
||||
}
|
||||
urls.BotSecret = c.BotSecretString()
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
func getAddr(host fmt.Stringer, port uint16) string {
|
||||
return net.JoinHostPort(host.String(), strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
// NewConfig returns new configuration. If required, it manages and
|
||||
// fetches data from external sources. Parameters passed to this
|
||||
// function, should come from command line arguments.
|
||||
func NewConfig(debug, verbose bool, // nolint: gocyclo
|
||||
bindIP net.IP, bindPort uint16,
|
||||
publicIPv4 net.IP, PublicIPv4Port uint16,
|
||||
publicIPv6 net.IP, publicIPv6Port uint16,
|
||||
statsIP net.IP, statsPort uint16,
|
||||
secret, adtag string,
|
||||
statsdIP string, statsdPort uint16, statsdNetwork string, statsdPrefix string,
|
||||
statsdTagsFormat string, statsdTags map[string]string) (*Config, error) {
|
||||
secureMode := false
|
||||
if strings.HasPrefix(secret, "dd") && len(secret) == 34 {
|
||||
secureMode = true
|
||||
secret = strings.TrimPrefix(secret, "dd")
|
||||
} else if len(secret) != 32 {
|
||||
return nil, errors.New("Telegram demands secret of length 32")
|
||||
}
|
||||
secretBytes, err := hex.DecodeString(secret)
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "Cannot create config")
|
||||
}
|
||||
|
||||
var adTagBytes []byte
|
||||
if len(adtag) != 0 {
|
||||
adTagBytes, err = hex.DecodeString(adtag)
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "Cannot create config")
|
||||
}
|
||||
}
|
||||
|
||||
if publicIPv4 == nil {
|
||||
publicIPv4, err = getGlobalIPv4()
|
||||
if err != nil {
|
||||
publicIPv4 = nil
|
||||
} else if publicIPv4.To4() == nil {
|
||||
return nil, errors.Errorf("IP %s is not IPv4", publicIPv4.String())
|
||||
}
|
||||
}
|
||||
if PublicIPv4Port == 0 {
|
||||
PublicIPv4Port = bindPort
|
||||
}
|
||||
|
||||
if publicIPv6 == nil {
|
||||
publicIPv6, err = getGlobalIPv6()
|
||||
if err != nil {
|
||||
publicIPv6 = nil
|
||||
} else if publicIPv6.To4() != nil {
|
||||
return nil, errors.Errorf("IP %s is not IPv6", publicIPv6.String())
|
||||
}
|
||||
}
|
||||
if publicIPv6Port == 0 {
|
||||
publicIPv6Port = bindPort
|
||||
}
|
||||
|
||||
if statsIP == nil {
|
||||
statsIP = publicIPv4
|
||||
}
|
||||
|
||||
conf := &Config{
|
||||
Debug: debug,
|
||||
Verbose: verbose,
|
||||
BindIP: bindIP,
|
||||
BindPort: bindPort,
|
||||
PublicIPv4: publicIPv4,
|
||||
PublicIPv4Port: PublicIPv4Port,
|
||||
PublicIPv6: publicIPv6,
|
||||
PublicIPv6Port: publicIPv6Port,
|
||||
StatsIP: statsIP,
|
||||
StatsPort: statsPort,
|
||||
Secret: secretBytes,
|
||||
AdTag: adTagBytes,
|
||||
SecureMode: secureMode,
|
||||
}
|
||||
|
||||
if statsdIP != "" {
|
||||
conf.StatsD.Enabled = true
|
||||
conf.StatsD.Prefix = statsdPrefix
|
||||
conf.StatsD.Tags = statsdTags
|
||||
|
||||
var addr net.Addr
|
||||
hostPort := net.JoinHostPort(statsdIP, strconv.Itoa(int(statsdPort)))
|
||||
switch statsdNetwork {
|
||||
case "tcp":
|
||||
addr, err = net.ResolveTCPAddr("tcp", hostPort)
|
||||
case "udp":
|
||||
addr, err = net.ResolveUDPAddr("udp", hostPort)
|
||||
default:
|
||||
err = errors.Errorf("Unknown network %s", statsdNetwork)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "Cannot resolve statsd address")
|
||||
}
|
||||
conf.StatsD.Addr = addr
|
||||
|
||||
switch statsdTagsFormat {
|
||||
case "datadog":
|
||||
conf.StatsD.TagsFormat = statsd.Datadog
|
||||
case "influxdb":
|
||||
conf.StatsD.TagsFormat = statsd.InfluxDB
|
||||
case "":
|
||||
default:
|
||||
return nil, errors.Errorf("Unknown tags format %s", statsdTagsFormat)
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/juju/errors"
|
||||
)
|
||||
|
||||
func getGlobalIPv4() (net.IP, error) {
|
||||
return fetchIP("https://v4.ifconfig.co/ip")
|
||||
}
|
||||
|
||||
func getGlobalIPv6() (net.IP, error) {
|
||||
return fetchIP("https://v6.ifconfig.co/ip")
|
||||
}
|
||||
|
||||
func fetchIP(url string) (net.IP, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
|
||||
respDataBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respData := strings.TrimSpace(string(respDataBytes))
|
||||
|
||||
ip := net.ParseIP(respData)
|
||||
if ip == nil {
|
||||
return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", respData)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func getURLs(addr net.IP, port uint16, secret string) (urls URLs) {
|
||||
values := url.Values{}
|
||||
values.Set("server", addr.String())
|
||||
values.Set("port", strconv.Itoa(int(port)))
|
||||
values.Set("secret", secret)
|
||||
|
||||
urls.TG = makeTGURL(values)
|
||||
urls.TMe = makeTMeURL(values)
|
||||
urls.TGQRCode = makeQRCodeURL(urls.TG)
|
||||
urls.TMeQRCode = makeQRCodeURL(urls.TG)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeTGURL(values url.Values) string {
|
||||
tgURL := url.URL{
|
||||
Scheme: "tg",
|
||||
Host: "proxy",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return tgURL.String()
|
||||
}
|
||||
|
||||
func makeTMeURL(values url.Values) string {
|
||||
tMeURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "t.me",
|
||||
Path: "proxy",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return tMeURL.String()
|
||||
}
|
||||
|
||||
func makeQRCodeURL(data string) string {
|
||||
QRURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.qrserver.com",
|
||||
Path: "v1/create-qr-code",
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("qzone", "4")
|
||||
values.Set("format", "svg")
|
||||
values.Set("data", data)
|
||||
QRURL.RawQuery = values.Encode()
|
||||
|
||||
return QRURL.String()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/OneOfOne/xxhash"
|
||||
)
|
||||
|
||||
// EventStream is a default implementation of the mtglib.EventStream
|
||||
// interface.
|
||||
//
|
||||
// EventStream manages a set of goroutines, observers. Main
|
||||
// responsibility of the event stream is to route an event to relevant
|
||||
// observer based on some hash so each observer will have all events
|
||||
// which belong to some stream id.
|
||||
//
|
||||
// Thus, EventStream can spawn many observers.
|
||||
type EventStream struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
chans []chan mtglib.Event
|
||||
}
|
||||
|
||||
// Send starts delivering of the message to observer with respect to a
|
||||
// given context If context is closed, message could be not delivered.
|
||||
func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
|
||||
var chanNo uint32
|
||||
|
||||
if streamID := evt.StreamID(); streamID != "" {
|
||||
chanNo = xxhash.ChecksumString32(streamID)
|
||||
} else {
|
||||
chanNo = rand.Uint32()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-e.ctx.Done():
|
||||
case e.chans[int(chanNo)%len(e.chans)] <- evt:
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops an event stream pipeline.
|
||||
func (e EventStream) Shutdown() {
|
||||
e.ctxCancel()
|
||||
}
|
||||
|
||||
// NewEventStream builds a new default event stream.
|
||||
//
|
||||
// If you give an empty array of observers, then NoopObserver is going
|
||||
// to be used. If you give many observers, then they will process a
|
||||
// message concurrently.
|
||||
func NewEventStream(observerFactories []ObserverFactory) EventStream {
|
||||
if len(observerFactories) == 0 {
|
||||
observerFactories = append(observerFactories, NewNoopObserver)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
rv := EventStream{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
chans: make([]chan mtglib.Event, runtime.NumCPU()),
|
||||
}
|
||||
|
||||
for i := 0; i < runtime.NumCPU(); i++ {
|
||||
rv.chans[i] = make(chan mtglib.Event, 1)
|
||||
|
||||
if len(observerFactories) == 1 {
|
||||
go eventStreamProcessor(ctx, rv.chans[i], observerFactories[0]())
|
||||
} else {
|
||||
go eventStreamProcessor(ctx, rv.chans[i], newMultiObserver(observerFactories))
|
||||
}
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { // nolint: cyclop
|
||||
defer observer.Shutdown()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case evt := <-eventChan:
|
||||
switch typedEvt := evt.(type) {
|
||||
case mtglib.EventTraffic:
|
||||
observer.EventTraffic(typedEvt)
|
||||
case mtglib.EventStart:
|
||||
observer.EventStart(typedEvt)
|
||||
case mtglib.EventFinish:
|
||||
observer.EventFinish(typedEvt)
|
||||
case mtglib.EventConnectedToDC:
|
||||
observer.EventConnectedToDC(typedEvt)
|
||||
case mtglib.EventDomainFronting:
|
||||
observer.EventDomainFronting(typedEvt)
|
||||
case mtglib.EventIPBlocklisted:
|
||||
observer.EventIPBlocklisted(typedEvt)
|
||||
case mtglib.EventConcurrencyLimited:
|
||||
observer.EventConcurrencyLimited(typedEvt)
|
||||
case mtglib.EventReplayAttack:
|
||||
observer.EventReplayAttack(typedEvt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type EventStreamTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
observerMock1 *ObserverMock
|
||||
observerMock2 *ObserverMock
|
||||
stream events.EventStream
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) SetupTest() {
|
||||
suite.ctx, suite.ctxCancel = context.WithCancel(context.Background())
|
||||
|
||||
suite.observerMock1 = &ObserverMock{}
|
||||
suite.observerMock2 = &ObserverMock{}
|
||||
|
||||
suite.observerMock1.On("Shutdown")
|
||||
suite.observerMock2.On("Shutdown")
|
||||
|
||||
factories := make([]events.ObserverFactory, 2)
|
||||
factories[0] = func() events.Observer { return suite.observerMock1 }
|
||||
factories[1] = func() events.Observer { return suite.observerMock2 }
|
||||
|
||||
suite.stream = events.NewEventStream(factories)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventStart() {
|
||||
evt := mtglib.NewEventStart("connID", net.ParseIP("10.0.0.1"))
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventStart", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventStart)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventConnectedToDC() {
|
||||
evt := mtglib.NewEventConnectedToDC("connID", net.ParseIP("10.0.0.1"), 3)
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventConnectedToDC", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventConnectedToDC)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.DC, caught.DC)
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventDomainFronting() {
|
||||
evt := mtglib.NewEventDomainFronting("connID")
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventDomainFronting", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventDomainFronting)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventTraffic() {
|
||||
evt := mtglib.NewEventTraffic("connID", 1024, true)
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventTraffic", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventTraffic)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
suite.Equal(evt.Traffic, caught.Traffic)
|
||||
suite.Equal(evt.IsRead, caught.IsRead)
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventFinish() {
|
||||
evt := mtglib.NewEventFinish("connID")
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventFinish", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventFinish)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventConcurrencyLimited() {
|
||||
evt := mtglib.NewEventConcurrencyLimited()
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventConcurrencyLimited", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventConcurrencyLimited)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
suite.Empty(evt.StreamID())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventIPBlocklisted() {
|
||||
evt := mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10"))
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventIPBlocklisted", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventIPBlocklisted)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TestEventReplayAttack() {
|
||||
evt := mtglib.NewEventReplayAttack("CONNID")
|
||||
|
||||
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||
v.
|
||||
On("EventReplayAttack", mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
caught, ok := args.Get(0).(mtglib.EventReplayAttack)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||
})
|
||||
}
|
||||
|
||||
suite.stream.Send(suite.ctx, evt)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *EventStreamTestSuite) TearDownTest() {
|
||||
suite.stream.Shutdown()
|
||||
suite.ctxCancel()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
suite.observerMock1.AssertExpectations(suite.T())
|
||||
suite.observerMock2.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func TestEventStream(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &EventStreamTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Events has a default implementations of EventStream for mtglib.
|
||||
//
|
||||
// Please see documentation for mtglib.EventStream interface to get an
|
||||
// idea of such an abstraction. This package has implementations for the
|
||||
// default event stream.
|
||||
//
|
||||
// Default event stream has a list of its own concepts. First, all it
|
||||
// does is a routing of messages to known observers. It takes an event,
|
||||
// defines its type and pass this message to a method of the observer.
|
||||
//
|
||||
// There might be many observers, but default event stream has a
|
||||
// guarantee though. It uses StreamID as a sharding key and guarantees
|
||||
// that a message with the same StreamID will be devlivered to the same
|
||||
// observer instance. So, each producer is guarateed to get all relevant
|
||||
// messages related to the same session. It is not possible that it will
|
||||
// get EventFinish if it has not seen EventStart for that session yet.
|
||||
package events
|
||||
|
||||
import "github.com/9seconds/mtg/v2/mtglib"
|
||||
|
||||
// Observer is an instance that listens for the incoming events.
|
||||
//
|
||||
// As it is said in the package description, the default event stream
|
||||
// guarantees that all events with the same StreamID are going to be
|
||||
// routed to the same instance of the observer. So, there is no need
|
||||
// to synchronize information about streams between many observers
|
||||
// instances, they can have their local storage.
|
||||
type Observer interface {
|
||||
// EventStart reacts on incoming mtglib.EventStart event.
|
||||
EventStart(mtglib.EventStart)
|
||||
|
||||
// EventFinish reacts on incoming mtglib.EventFinish event.
|
||||
EventFinish(mtglib.EventFinish)
|
||||
|
||||
// EventConnectedToDC reacts on incoming mtglib.EventConnectedToDC
|
||||
// event.
|
||||
EventConnectedToDC(mtglib.EventConnectedToDC)
|
||||
|
||||
// EventDomainFronting reacts on incoming mtglib.EventDomainFronting
|
||||
// event.
|
||||
EventDomainFronting(mtglib.EventDomainFronting)
|
||||
|
||||
// EventTraffic reacts on incoming mtglib.EventTraffic event.
|
||||
EventTraffic(mtglib.EventTraffic)
|
||||
|
||||
// EventConcurrencyLimited reacts on incoming
|
||||
// mtglib.EventConcurrencyLimited event.
|
||||
EventConcurrencyLimited(mtglib.EventConcurrencyLimited)
|
||||
|
||||
// EventIPBlocklisted reacts on incoming mtglib.EventIPBlocklisted event.
|
||||
EventIPBlocklisted(mtglib.EventIPBlocklisted)
|
||||
|
||||
// EventReplayAttack reacts on incoming mtglib.EventReplayAttack event.
|
||||
EventReplayAttack(mtglib.EventReplayAttack)
|
||||
|
||||
// Shutdown stop observer. Default event stream guarantees:
|
||||
// 1. If shutdown is executed, it is executed only once
|
||||
// 2. Observer won't receieve any new message after this
|
||||
// function call.
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// ObserverFactory creates a new instance of the observer.
|
||||
//
|
||||
// Default event stream creates a small set of goroutines to manage
|
||||
// incoming messages. Each message is routed to an appropriate observer
|
||||
// based on a sharding key, stream id. So, it is possible that an
|
||||
// instance of mtg will have many observer instances, not a single one.
|
||||
type ObserverFactory func() Observer
|
||||
@@ -0,0 +1,46 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type ObserverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventStart(evt mtglib.EventStart) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventDomainFronting(evt mtglib.EventDomainFronting) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventTraffic(evt mtglib.EventTraffic) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventFinish(evt mtglib.EventFinish) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) {
|
||||
o.Called(evt)
|
||||
}
|
||||
|
||||
func (o *ObserverMock) Shutdown() {
|
||||
o.Called()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type multiObserver struct {
|
||||
observers []Observer
|
||||
}
|
||||
|
||||
func (m multiObserver) EventStart(evt mtglib.EventStart) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventStart(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventConnectedToDC(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventDomainFronting(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventTraffic(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventFinish(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventConcurrencyLimited(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventIPBlocklisted(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(m.observers))
|
||||
|
||||
for _, v := range m.observers {
|
||||
go func(obs Observer) {
|
||||
defer wg.Done()
|
||||
|
||||
obs.EventReplayAttack(evt)
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m multiObserver) Shutdown() {
|
||||
for _, v := range m.observers {
|
||||
v.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func newMultiObserver(factories []ObserverFactory) Observer {
|
||||
observers := make([]Observer, len(factories))
|
||||
|
||||
for i, v := range factories {
|
||||
observers[i] = v()
|
||||
}
|
||||
|
||||
return multiObserver{
|
||||
observers: observers,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) Send(ctx context.Context, evt mtglib.Event) {}
|
||||
|
||||
// NewNoopStream creates a stream which discards each message.
|
||||
func NewNoopStream() mtglib.EventStream {
|
||||
return noop{}
|
||||
}
|
||||
|
||||
type noopObserver struct{}
|
||||
|
||||
func (n noopObserver) EventStart(_ mtglib.EventStart) {}
|
||||
func (n noopObserver) EventConnectedToDC(_ mtglib.EventConnectedToDC) {}
|
||||
func (n noopObserver) EventDomainFronting(_ mtglib.EventDomainFronting) {}
|
||||
func (n noopObserver) EventTraffic(_ mtglib.EventTraffic) {}
|
||||
func (n noopObserver) EventFinish(_ mtglib.EventFinish) {}
|
||||
func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {}
|
||||
func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) {}
|
||||
func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack) {}
|
||||
func (n noopObserver) Shutdown() {}
|
||||
|
||||
// NewNoopObserver creates an observer which discards each message.
|
||||
func NewNoopObserver() Observer {
|
||||
return noopObserver{}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
testData map[string]mtglib.Event
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) SetupSuite() {
|
||||
suite.testData = map[string]mtglib.Event{
|
||||
"start": mtglib.NewEventStart("connID", net.ParseIP("127.0.0.1")),
|
||||
"connected-to-dc": mtglib.NewEventConnectedToDC("connID", net.ParseIP("127.1.0.1"), 2),
|
||||
"domain-fronting": mtglib.NewEventDomainFronting("connID"),
|
||||
"traffic": mtglib.NewEventTraffic("connID", 1000, true),
|
||||
"finish": mtglib.NewEventFinish("connID"),
|
||||
"concurrency-limited": mtglib.NewEventConcurrencyLimited(),
|
||||
"ip-blacklisted": mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")),
|
||||
"replay-attack": mtglib.NewEventReplayAttack("connID"),
|
||||
}
|
||||
suite.ctx = context.Background()
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestStream() {
|
||||
stream := events.NewNoopStream()
|
||||
|
||||
for name, v := range suite.testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
stream.Send(suite.ctx, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestObserver() {
|
||||
observer := events.NewNoopObserver()
|
||||
|
||||
for name, v := range suite.testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
switch typedEvt := value.(type) {
|
||||
case mtglib.EventStart:
|
||||
observer.EventStart(typedEvt)
|
||||
case mtglib.EventConnectedToDC:
|
||||
observer.EventConnectedToDC(typedEvt)
|
||||
case mtglib.EventDomainFronting:
|
||||
observer.EventDomainFronting(typedEvt)
|
||||
case mtglib.EventFinish:
|
||||
observer.EventFinish(typedEvt)
|
||||
case mtglib.EventConcurrencyLimited:
|
||||
observer.EventConcurrencyLimited(typedEvt)
|
||||
case mtglib.EventIPBlocklisted:
|
||||
observer.EventIPBlocklisted(typedEvt)
|
||||
case mtglib.EventReplayAttack:
|
||||
observer.EventReplayAttack(typedEvt)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
observer.Shutdown()
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
# This is an example of the configuration file for mtg. You actually can
|
||||
# run mtg with it. It starts a proxy on all interfaces with a secret
|
||||
# ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d
|
||||
#
|
||||
# It has all possible options with default values. So, a real world
|
||||
# configuration file should contain only those options you are going to
|
||||
# use. You do not need to enumerate all of them. In other words, each
|
||||
# option here has a default value. If you comment a key-value pair, it
|
||||
# should not make any effect.
|
||||
#
|
||||
# stats is the only exception.
|
||||
|
||||
# Debug starts application in debug mode. It starts to be quite verbose
|
||||
# in output. Actually, the idea is that you run it in debug mode only if
|
||||
# you have any issue.
|
||||
debug = true
|
||||
|
||||
# A secret. Please remember that mtg supports only FakeTLS mode, legacy
|
||||
# simple and secured mode are prohibited. For you it means that secret
|
||||
# should either be base64-encoded or starts with ee.
|
||||
secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d"
|
||||
|
||||
# Host:port pair to run proxy on.
|
||||
bind-to = "0.0.0.0:3128"
|
||||
|
||||
# Defines how many concurrent connections are allowed to this proxy.
|
||||
# All other incoming connections are going to be dropped.
|
||||
concurrency = 8192
|
||||
|
||||
# A size of user-space buffer for TCP to use. Since we do 2 connections,
|
||||
# then we have tcp-buffer * (4 + 2) per each connection: read/write for
|
||||
# each connection + 2 copy buffers to pump the data between sockets.
|
||||
tcp-buffer = "4kb"
|
||||
|
||||
# Sometimes you want to enforce mtg to use some types of
|
||||
# IP connectivity to Telegram. We have 4 modes:
|
||||
# - prefer-ipv6:
|
||||
# We can use both ipv4 and ipv6 but ipv6 has a preference
|
||||
# - prefer-ipv4:
|
||||
# We can use both ipv4 and ipv6 but ipv4 has a preference
|
||||
# - only-ipv6:
|
||||
# Only ipv6 connectivity is used
|
||||
# - only-ipv4:
|
||||
# Only ipv4 connectivity is used
|
||||
prefer-ip = "prefer-ipv6"
|
||||
|
||||
# FakeTLS uses domain fronting protection. So it needs to know a port to
|
||||
# access.
|
||||
domain-fronting-port = 443
|
||||
|
||||
# FakeTLS can compare timestamps to prevent probes. Each message has
|
||||
# encrypted timestamp. So, mtg can compare this timestamp and decide if
|
||||
# we need to proceed with connection or not.
|
||||
#
|
||||
# Sometimes time can be skewed so we accept all messages within a
|
||||
# time range of this parameter.
|
||||
tolerate-time-skewness = "5s"
|
||||
|
||||
# network defines different network-related settings
|
||||
[network]
|
||||
# please be aware that mtg needs to do some external requests. For
|
||||
# example, if you do not pass public ips, it will request your public ip
|
||||
# address from some external service.
|
||||
#
|
||||
# As for 2.0, if you set a public-ip on your own, mtg won't issue any
|
||||
# network requests except of those required for Telegram.
|
||||
#
|
||||
# so, in order of doing them, it needs to do DNS lookup. mtg ignores DNS
|
||||
# resolver of the operating system and uses DOH instead. This is a host
|
||||
# it has to access.
|
||||
#
|
||||
# By default we use Quad9.
|
||||
doh-ip = "9.9.9.9"
|
||||
|
||||
# mtg can work via proxies (for now, we support only socks5). Proxy
|
||||
# configuration is done via list. So, you can specify many proxies
|
||||
# there.
|
||||
#
|
||||
# Actually, if you supply an empty list, then no proxies are going to be
|
||||
# used. If you supply a single proxy, then mtg will use it exclusively.
|
||||
# If you supply >= 2, then mtg will load balance between them.
|
||||
#
|
||||
# If you add an empty string here, this is an equivalent of 'plain network',
|
||||
# with no proxy usage.
|
||||
#
|
||||
# Proxy configuration is done via ordinary URI schema:
|
||||
#
|
||||
# socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s
|
||||
#
|
||||
# Only socks5 proxy is used. user/password is optional. As you can
|
||||
# see, you can specify some parameters in GET query. These parameters
|
||||
# configure circuit breaker.
|
||||
#
|
||||
# open_threshold means a number of errors which should happen so we stop
|
||||
# use a proxy.
|
||||
#
|
||||
# half_open_timeout means a time period (in Golang duration notation)
|
||||
# after which we can retry with this proxy
|
||||
#
|
||||
# reset_failures_timeout means a time period when we flush out errors
|
||||
# when circuit breaker in closed state.
|
||||
#
|
||||
# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
|
||||
# on details about circuit breakers.
|
||||
proxies = [
|
||||
# "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s"
|
||||
]
|
||||
|
||||
# network timeouts define different settings for timeouts. tcp timeout
|
||||
# define a global timeout on establishing of network connections. idle
|
||||
# means a timeout on pumping data between sockset when nothing is
|
||||
# happening.
|
||||
#
|
||||
# please be noticed that handshakes have no timeouts intentionally. You can
|
||||
# find a reasoning here:
|
||||
# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf
|
||||
[network.timeout]
|
||||
tcp = "5s"
|
||||
http = "10s"
|
||||
idle = "1m"
|
||||
|
||||
# Some countries do active probing on Telegram connections. This technique
|
||||
# allows to protect from such effort.
|
||||
#
|
||||
# mtg has a cache of some connection fingerprints. Actually, first bytes
|
||||
# of each connection. So, it stores them in some in-memory LRU+TTL cache.
|
||||
# You can configure this cache here.
|
||||
[defense.anti-replay]
|
||||
# You can enable/disable this feature.
|
||||
enabled = true
|
||||
# max size of such a cache. Please be aware that this number is
|
||||
# approximate we try hard to store data quite dense but it is possible
|
||||
# that we can go over this limit for 10-20% under some conditions and
|
||||
# architectures.
|
||||
max-size = "1mib"
|
||||
# we use stable bloom filters for anti-replay cache. This helps
|
||||
# to maintain a desired error ratio.
|
||||
error-rate = 0.001
|
||||
|
||||
# You can protect proxies by using different blocklists. If client has
|
||||
# ip from the given range, we do not try to do a proper handshake. We
|
||||
# actually route it to fronting domain. So, this client will never ever
|
||||
# have a chance to use mtg to access Telegram.
|
||||
#
|
||||
# Please remember that blocklists are initialized in async way. So,
|
||||
# when you start a proxy, blocklists are empty, they are populated and
|
||||
# processed in backgrounds. An error in any URL is ignored.
|
||||
[defense.blocklist]
|
||||
# You can enable/disable this feature.
|
||||
enabled = true
|
||||
# This is a limiter for concurrency. In order to protect website
|
||||
# from overloading, we download files in this number of threads.
|
||||
download-concurrency = 2
|
||||
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
|
||||
# You can provider links here (starts with https:// or http://) or
|
||||
# path to a local file, but in this case it should be absolute.
|
||||
urls = [
|
||||
# "https://iplists.firehol.org/files/firehol_level1.netset",
|
||||
# "/local.file"
|
||||
]
|
||||
# How often do we need to update a blocklist set.
|
||||
update-each = "24h"
|
||||
|
||||
# statsd statistics integration.
|
||||
[stats.statsd]
|
||||
# enabled/disabled
|
||||
enabled = false
|
||||
# host:port for UDP endpoint of statsd
|
||||
address = "127.0.0.1:8888"
|
||||
# prefix of metric for statsd
|
||||
metric-prefix = "mtg"
|
||||
# tag format to use
|
||||
# supported values are 'datadog', 'influxdb' and 'graphite'
|
||||
# default format is graphite.
|
||||
tag-format = "datadog"
|
||||
|
||||
# prometheus metrics integration.
|
||||
[stats.prometheus]
|
||||
# enabled/disabled
|
||||
enabled = true
|
||||
# host:port where to start http server for endpoint
|
||||
bind-to = "127.0.0.1:3129"
|
||||
# prefix of http path
|
||||
http-path = "/"
|
||||
# prefix for metrics for prometheus
|
||||
metric-prefix = "mtg"
|
||||
@@ -0,0 +1,30 @@
|
||||
module github.com/9seconds/mtg/v2
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
github.com/alecthomas/kong v0.2.16
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/jarcoal/httpmock v1.0.8
|
||||
github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1
|
||||
github.com/libp2p/go-reuseport v0.0.2
|
||||
github.com/mccutchen/go-httpbin v1.1.1
|
||||
github.com/panjf2000/ants/v2 v2.4.4
|
||||
github.com/pelletier/go-toml v1.9.0
|
||||
github.com/prometheus/client_golang v1.10.0
|
||||
github.com/prometheus/common v0.20.0 // indirect
|
||||
github.com/rs/zerolog v1.21.0
|
||||
github.com/smira/go-statsd v1.3.2
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
|
||||
)
|
||||
@@ -0,0 +1,482 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ=
|
||||
github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/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/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/go-systemd v0.0.0-20190321100706-95778dfbb74e/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/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/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/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/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/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
|
||||
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
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/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1 h1:D7qhJP3R49ZjUzpzKQ6B2H3lgejPs6DTO5gRomhhOpE=
|
||||
github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1/go.mod h1:2OfLA+0esiUJpwMjrH39pEk79cb8MvGTBS9YlZpejJ4=
|
||||
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/libp2p/go-reuseport v0.0.2 h1:XSG94b1FJfGA01BUrT82imejHQyTxO4jEWqheyCXYvU=
|
||||
github.com/libp2p/go-reuseport v0.0.2/go.mod h1:SPD+5RwGC7rcnzngoYC86GjPzjSywuQyMVAheVBD9nQ=
|
||||
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/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
|
||||
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
|
||||
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/panjf2000/ants/v2 v2.4.4 h1:kebk2KSiXHGeiYS6b+w2RqNN5+IKoqlBNd7cuC7MvQI=
|
||||
github.com/panjf2000/ants/v2 v2.4.4/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
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/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=
|
||||
github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg=
|
||||
github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
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.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/common v0.20.0 h1:pfeDeUdQcIxOMutNjCejsEFp7qeP+/iltHSSmLpE+hU=
|
||||
github.com/prometheus/common v0.20.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
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/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM=
|
||||
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
|
||||
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.2 h1:1EeuzxNZ/TD9apbTOFSM9nulqfcsQFmT4u1A2DREabI=
|
||||
github.com/smira/go-statsd v1.3.2/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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.1.5-0.20170809224252-890a5c3458b4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg=
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
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=
|
||||
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/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-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-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-20190228124157-a34e9553db1e/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-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/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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.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/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/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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
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=
|
||||
@@ -0,0 +1,192 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
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"`
|
||||
TgQrCode string `json:"tg_qrcode"`
|
||||
TmeURL string `json:"tme_url"`
|
||||
TmeQrCode string `json:"tme_qrcode"`
|
||||
}
|
||||
|
||||
type Access struct {
|
||||
base
|
||||
|
||||
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 (c *Access) Run(cli *CLI, version string) error {
|
||||
if err := c.ReadConfig(version); err != nil {
|
||||
return fmt.Errorf("cannot init config: %w", err)
|
||||
}
|
||||
|
||||
return c.Execute(cli)
|
||||
}
|
||||
|
||||
func (c *Access) Execute(cli *CLI) error {
|
||||
resp := &accessResponse{}
|
||||
resp.Secret.Base64 = c.Config.Secret.Base64()
|
||||
resp.Secret.Hex = c.Config.Secret.Hex()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2) // nolint: gomnd
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ip := cli.Access.PublicIPv4
|
||||
if ip == nil {
|
||||
ip = c.getIP("tcp4")
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
|
||||
resp.IPv4 = c.makeURLs(ip, cli)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ip := cli.Access.PublicIPv6
|
||||
if ip == nil {
|
||||
ip = c.getIP("tcp6")
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
ip = ip.To16()
|
||||
}
|
||||
|
||||
resp.IPv6 = c.makeURLs(ip, cli)
|
||||
}()
|
||||
|
||||
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 (c *Access) getIP(protocol string) net.IP {
|
||||
client := c.Network.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return c.Network.DialContext(ctx, protocol, address) // nolint: wrapcheck
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "text/plain")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
io.Copy(io.Discard, resp.Body) // nolint: errcheck
|
||||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return net.ParseIP(strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
func (c *Access) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
portNo := cli.Access.Port
|
||||
if portNo == 0 {
|
||||
portNo = c.Config.BindTo.PortValue(0)
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("server", ip.String())
|
||||
values.Set("port", strconv.Itoa(int(portNo)))
|
||||
|
||||
if cli.Access.Hex {
|
||||
values.Set("secret", c.Config.Secret.Hex())
|
||||
} else {
|
||||
values.Set("secret", c.Config.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 = c.makeQRCode(rv.TgURL)
|
||||
rv.TmeQrCode = c.makeQRCode(rv.TmeURL)
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
func (c *Access) makeQRCode(data string) string {
|
||||
values := url.Values{}
|
||||
values.Set("qzone", "4")
|
||||
values.Set("format", "svg")
|
||||
values.Set("data", data)
|
||||
|
||||
return (&url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.qrserver.com",
|
||||
Path: "v1/create-qr-code",
|
||||
RawQuery: values.Encode(),
|
||||
}).String()
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
)
|
||||
|
||||
var accressResponseJSONSchema = func() *gojsonschema.Schema {
|
||||
schema, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(`
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["secret"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"hex",
|
||||
"base64"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"hex": {
|
||||
"type": "string",
|
||||
"minLength": 34
|
||||
},
|
||||
"base64": {
|
||||
"type": "string",
|
||||
"minLength": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"ipv4": {
|
||||
"$ref": "#/definitions/ip"
|
||||
},
|
||||
"ipv6": {
|
||||
"$ref": "#/definitions/ip"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"ip": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ip",
|
||||
"port",
|
||||
"tg_url",
|
||||
"tg_qrcode",
|
||||
"tme_url",
|
||||
"tme_qrcode"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "ipv4"
|
||||
},
|
||||
{
|
||||
"format": "ipv6"
|
||||
}
|
||||
]
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"multipleOf": 1.0,
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 65536
|
||||
},
|
||||
"tg_url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"format": "uri"
|
||||
},
|
||||
"tg_qrcode": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"format": "uri"
|
||||
},
|
||||
"tme_url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"format": "uri"
|
||||
},
|
||||
"tme_qrcode": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return schema
|
||||
}()
|
||||
|
||||
type AccessTestSuite struct {
|
||||
CommonTestSuite
|
||||
}
|
||||
|
||||
func (suite *AccessTestSuite) SetupTest() {
|
||||
suite.CommonTestSuite.SetupTest()
|
||||
|
||||
suite.cli.Access.Config = &config.Config{}
|
||||
suite.cli.Access.Config.Secret = mtglib.GenerateSecret("google.com")
|
||||
suite.cli.Access.Network = suite.networkMock
|
||||
|
||||
suite.NoError(
|
||||
suite.cli.Access.Config.BindTo.UnmarshalText([]byte("0.0.0.0:80")))
|
||||
}
|
||||
|
||||
func (suite *AccessTestSuite) TestGenerateNoCalls() {
|
||||
suite.cli.Access.PublicIPv4 = net.ParseIP("10.0.0.10")
|
||||
suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||
|
||||
output := testlib.CaptureStdout(func() {
|
||||
suite.NoError(suite.cli.Access.Execute(suite.cli))
|
||||
})
|
||||
|
||||
validated, err := accressResponseJSONSchema.Validate(
|
||||
gojsonschema.NewStringLoader(output))
|
||||
suite.NoError(err)
|
||||
suite.Empty(validated.Errors())
|
||||
suite.True(validated.Valid())
|
||||
|
||||
suite.Contains(output, "10.0.0.10")
|
||||
suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
|
||||
suite.Contains(output, "ipv4")
|
||||
suite.Contains(output, "ipv6")
|
||||
suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
|
||||
suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
|
||||
}
|
||||
|
||||
func (suite *AccessTestSuite) TestGenerateIPv4Call() {
|
||||
suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||
|
||||
httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co",
|
||||
httpmock.NewStringResponder(http.StatusOK, "10.11.12.13"))
|
||||
|
||||
output := testlib.CaptureStdout(func() {
|
||||
suite.NoError(suite.cli.Access.Execute(suite.cli))
|
||||
})
|
||||
|
||||
validated, err := accressResponseJSONSchema.Validate(
|
||||
gojsonschema.NewStringLoader(output))
|
||||
suite.NoError(err)
|
||||
suite.Empty(validated.Errors())
|
||||
suite.True(validated.Valid())
|
||||
|
||||
suite.Contains(output, "10.11.12.13")
|
||||
suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
|
||||
suite.Contains(output, "ipv4")
|
||||
suite.Contains(output, "ipv6")
|
||||
suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
|
||||
suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
|
||||
}
|
||||
|
||||
func (suite *AccessTestSuite) TestIPv4CallFail() {
|
||||
suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||
|
||||
httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co",
|
||||
httpmock.NewStringResponder(http.StatusForbidden, ""))
|
||||
|
||||
output := testlib.CaptureStdout(func() {
|
||||
suite.NoError(suite.cli.Access.Execute(suite.cli))
|
||||
})
|
||||
|
||||
validated, err := accressResponseJSONSchema.Validate(
|
||||
gojsonschema.NewStringLoader(output))
|
||||
suite.NoError(err)
|
||||
suite.Empty(validated.Errors())
|
||||
suite.True(validated.Valid())
|
||||
|
||||
suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
|
||||
suite.NotContains(output, "ipv4")
|
||||
suite.Contains(output, "ipv6")
|
||||
suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
|
||||
suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
|
||||
}
|
||||
|
||||
func TestAccess(t *testing.T) { // nolint: paralleltest
|
||||
suite.Run(t, &AccessTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/9seconds/mtg/v2/network"
|
||||
)
|
||||
|
||||
type base struct {
|
||||
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
|
||||
|
||||
Network mtglib.Network `kong:"-"`
|
||||
Config *config.Config `kong:"-"`
|
||||
}
|
||||
|
||||
func (b *base) ReadConfig(version string) error {
|
||||
content, err := os.ReadFile(b.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read config file: %w", err)
|
||||
}
|
||||
|
||||
conf, err := config.Parse(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse config: %w", err)
|
||||
}
|
||||
|
||||
ntw, err := b.makeNetwork(conf, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build a network: %w", err)
|
||||
}
|
||||
|
||||
b.Config = conf
|
||||
b.Network = ntw
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *base) makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
|
||||
tcpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultTimeout)
|
||||
httpTimeout := conf.Network.Timeout.HTTP.Value(network.DefaultHTTPTimeout)
|
||||
dohIP := conf.Network.DOHIP.Value(net.ParseIP(network.DefaultDOHHostname)).String()
|
||||
bufferSize := conf.TCPBuffer.Value(network.DefaultBufferSize)
|
||||
userAgent := "mtg/" + version
|
||||
|
||||
baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build a default dialer: %w", err)
|
||||
}
|
||||
|
||||
proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
|
||||
|
||||
for _, v := range conf.Network.Proxies {
|
||||
if value := v.Value(nil); value != nil {
|
||||
proxyURLs = append(proxyURLs, v.Value(nil))
|
||||
}
|
||||
}
|
||||
|
||||
switch len(proxyURLs) {
|
||||
case 0:
|
||||
return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
case 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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type BaseTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
b base
|
||||
}
|
||||
|
||||
func (suite *BaseTestSuite) SetupTest() {
|
||||
suite.b = base{}
|
||||
}
|
||||
|
||||
func (suite *BaseTestSuite) TestReadConfigNok() {
|
||||
suite.b.ConfigPath = filepath.Join("testdata", "unknown")
|
||||
suite.Error(suite.b.ReadConfig("dev"))
|
||||
}
|
||||
|
||||
func (suite *BaseTestSuite) TestReadConfig() {
|
||||
suite.b.ConfigPath = filepath.Join("testdata", "minimal.toml")
|
||||
suite.NoError(suite.b.ReadConfig("dev"))
|
||||
}
|
||||
|
||||
func TestBase(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &BaseTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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 Proxy `kong:"cmd,help='Run proxy.'"`
|
||||
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 (c *GenerateSecret) Run(cli *CLI, _ string) error {
|
||||
secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
|
||||
|
||||
if cli.GenerateSecret.Hex {
|
||||
fmt.Println(secret.Hex()) // nolint: forbidigo
|
||||
} else {
|
||||
fmt.Println(secret.Base64()) // nolint: forbidigo
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type GenerateSecretTestSuite struct {
|
||||
CommonTestSuite
|
||||
}
|
||||
|
||||
func (suite *GenerateSecretTestSuite) SetupTest() {
|
||||
suite.CommonTestSuite.SetupTest()
|
||||
|
||||
suite.cli.GenerateSecret.HostName = "google.com"
|
||||
}
|
||||
|
||||
func (suite *GenerateSecretTestSuite) TestDefault() {
|
||||
output := testlib.CaptureStdout(func() {
|
||||
suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev"))
|
||||
})
|
||||
suite.True(strings.HasPrefix(output, "7"))
|
||||
|
||||
secret, err := mtglib.ParseSecret(output)
|
||||
suite.NoError(err)
|
||||
suite.True(secret.Valid())
|
||||
suite.Equal("google.com", secret.Host)
|
||||
}
|
||||
|
||||
func (suite *GenerateSecretTestSuite) TestHex() {
|
||||
suite.cli.GenerateSecret.Hex = true
|
||||
|
||||
output := testlib.CaptureStdout(func() {
|
||||
suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev"))
|
||||
})
|
||||
suite.True(strings.HasPrefix(output, "ee"))
|
||||
|
||||
secret, err := mtglib.ParseSecret(output)
|
||||
suite.NoError(err)
|
||||
suite.True(secret.Valid())
|
||||
suite.Equal("google.com", secret.Host)
|
||||
}
|
||||
|
||||
func TestGenerateSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &GenerateSecretTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/cli"
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CommonTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
cli *cli.CLI
|
||||
networkMock *testlib.MtglibNetworkMock
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func (suite *CommonTestSuite) SetupTest() {
|
||||
suite.networkMock = &testlib.MtglibNetworkMock{}
|
||||
suite.httpClient = &http.Client{}
|
||||
suite.cli = &cli.CLI{}
|
||||
|
||||
httpmock.ActivateNonDefault(suite.httpClient)
|
||||
|
||||
suite.networkMock.
|
||||
On("MakeHTTPClient", mock.Anything).
|
||||
Maybe().
|
||||
Return(suite.httpClient)
|
||||
}
|
||||
|
||||
func (suite *CommonTestSuite) TearDownTest() {
|
||||
suite.networkMock.AssertExpectations(suite.T())
|
||||
httpmock.DeactivateAndReset()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/9seconds/mtg/v2/stats"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
base
|
||||
}
|
||||
|
||||
func (c *Proxy) Run(cli *CLI, version string) error {
|
||||
if err := c.ReadConfig(version); err != nil {
|
||||
return fmt.Errorf("cannot init config: %w", err)
|
||||
}
|
||||
|
||||
return c.Execute()
|
||||
}
|
||||
|
||||
func (c *Proxy) Execute() error {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFieldName = "level"
|
||||
|
||||
if c.Config.Debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
} else {
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
}
|
||||
|
||||
ctx := utils.RootContext()
|
||||
opts := mtglib.ProxyOpts{
|
||||
Logger: logger.NewZeroLogger(zerolog.New(os.Stdout).With().Timestamp().Logger()),
|
||||
Network: c.Network,
|
||||
AntiReplayCache: antireplay.NewNoop(),
|
||||
IPBlocklist: ipblocklist.NewNoop(),
|
||||
EventStream: events.NewNoopStream(),
|
||||
|
||||
Secret: c.Config.Secret,
|
||||
BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize),
|
||||
DomainFrontingPort: c.Config.DomainFrontingPort.Value(mtglib.DefaultDomainFrontingPort),
|
||||
IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout),
|
||||
PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP),
|
||||
}
|
||||
|
||||
opts.Logger.BindStr("configuration", c.Config.String()).Debug("configuration")
|
||||
|
||||
c.setupAntiReplayCache(&opts)
|
||||
|
||||
if err := c.setupIPBlocklist(&opts); err != nil {
|
||||
return fmt.Errorf("cannot setup ipblocklist: %w", err)
|
||||
}
|
||||
|
||||
if err := c.setupEventStream(&opts); err != nil {
|
||||
return fmt.Errorf("cannot setup event stream: %w", err)
|
||||
}
|
||||
|
||||
proxy, err := mtglib.NewProxy(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create a proxy: %w", err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", c.Config.BindTo.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start proxy: %w", err)
|
||||
}
|
||||
|
||||
go proxy.Serve(listener) // nolint: errcheck
|
||||
|
||||
<-ctx.Done()
|
||||
listener.Close()
|
||||
proxy.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Proxy) setupAntiReplayCache(opts *mtglib.ProxyOpts) {
|
||||
if !c.Config.Defense.AntiReplay.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
opts.AntiReplayCache = antireplay.NewStableBloomFilter(
|
||||
c.Config.Defense.AntiReplay.MaxSize.Value(antireplay.DefaultStableBloomFilterMaxSize),
|
||||
c.Config.Defense.AntiReplay.ErrorRate.Value(antireplay.DefaultStableBloomFilterErrorRate),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Proxy) setupIPBlocklist(opts *mtglib.ProxyOpts) error {
|
||||
if !c.Config.Defense.Blocklist.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
remoteURLs := []string{}
|
||||
localFiles := []string{}
|
||||
|
||||
for _, v := range c.Config.Defense.Blocklist.URLs {
|
||||
if v.IsRemote() {
|
||||
remoteURLs = append(remoteURLs, v.String())
|
||||
} else {
|
||||
localFiles = append(localFiles, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
firehol, err := ipblocklist.NewFirehol(opts.Logger.Named("ipblockist"),
|
||||
c.Network,
|
||||
c.Config.Defense.Blocklist.DownloadConcurrency,
|
||||
remoteURLs,
|
||||
localFiles)
|
||||
if err != nil {
|
||||
return err // nolint: wrapcheck
|
||||
}
|
||||
|
||||
go firehol.Run(c.Config.Defense.Blocklist.UpdateEach.Value(ipblocklist.DefaultFireholUpdateEach))
|
||||
|
||||
opts.IPBlocklist = firehol
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Proxy) setupEventStream(opts *mtglib.ProxyOpts) error {
|
||||
factories := make([]events.ObserverFactory, 0, 2)
|
||||
|
||||
if c.Config.Stats.StatsD.Enabled {
|
||||
statsdFactory, err := stats.NewStatsd(
|
||||
c.Config.Stats.StatsD.Address.String(),
|
||||
opts.Logger.Named("statsd"),
|
||||
c.Config.Stats.StatsD.MetricPrefix.Value(stats.DefaultStatsdMetricPrefix),
|
||||
c.Config.Stats.StatsD.TagFormat.Value(stats.DefaultStatsdTagFormat))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build statsd observer: %w", err)
|
||||
}
|
||||
|
||||
factories = append(factories, statsdFactory.Make)
|
||||
}
|
||||
|
||||
if c.Config.Stats.Prometheus.Enabled {
|
||||
prometheus := stats.NewPrometheus(
|
||||
c.Config.Stats.Prometheus.MetricPrefix.Value(stats.DefaultMetricPrefix),
|
||||
c.Config.Stats.Prometheus.HTTPPath.Value("/"),
|
||||
)
|
||||
|
||||
listener, err := net.Listen("tcp", c.Config.Stats.Prometheus.BindTo.String())
|
||||
if err != nil {
|
||||
return 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 {
|
||||
opts.EventStream = events.NewEventStream(factories)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t"
|
||||
bind-to = "0.0.0.0:80"
|
||||
@@ -0,0 +1,157 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Debug bool `json:"debug"`
|
||||
Secret mtglib.Secret `json:"secret"`
|
||||
BindTo TypeHostPort `json:"bind-to"`
|
||||
TCPBuffer TypeBytes `json:"tcp-buffer"`
|
||||
PreferIP TypePreferIP `json:"prefer-ip"`
|
||||
DomainFrontingPort TypePort `json:"domain-fronting-port"`
|
||||
TolerateTimeSkewness TypeDuration `json:"tolerate-time-skewness"`
|
||||
Concurrency uint `json:"concurrency"`
|
||||
Defense struct {
|
||||
AntiReplay struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MaxSize TypeBytes `json:"max-size"`
|
||||
ErrorRate TypeErrorRate `json:"error-rate"`
|
||||
} `json:"anti-replay"`
|
||||
Blocklist struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DownloadConcurrency uint `json:"download-concurrency"`
|
||||
URLs []TypeBlocklistURI `json:"urls"`
|
||||
UpdateEach TypeDuration `json:"update-each"`
|
||||
} `json:"blocklist"`
|
||||
} `json:"defense"`
|
||||
Network struct {
|
||||
Timeout struct {
|
||||
TCP TypeDuration `json:"tcp"`
|
||||
HTTP TypeDuration `json:"http"`
|
||||
Idle TypeDuration `json:"idle"`
|
||||
} `json:"timeout"`
|
||||
DOHIP TypeIP `json:"doh-ip"`
|
||||
Proxies []TypeURL `json:"proxies"`
|
||||
} `json:"network"`
|
||||
Stats struct {
|
||||
StatsD struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Address TypeHostPort `json:"address"`
|
||||
MetricPrefix TypeMetricPrefix `json:"metric-prefix"`
|
||||
TagFormat TypeStatsdTagFormat `json:"tag-format"`
|
||||
} `json:"statsd"`
|
||||
Prometheus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BindTo TypeHostPort `json:"bind-to"`
|
||||
HTTPPath TypeHTTPPath `json:"http-path"`
|
||||
MetricPrefix TypeMetricPrefix `json:"metric-prefix"`
|
||||
} `json:"prometheus"`
|
||||
} `json:"stats"`
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if !c.Secret.Valid() {
|
||||
return fmt.Errorf("invalid secret %s", c.Secret.String())
|
||||
}
|
||||
|
||||
if len(c.BindTo.HostValue(nil)) == 0 || c.BindTo.PortValue(0) == 0 {
|
||||
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()
|
||||
}
|
||||
|
||||
type configRaw struct {
|
||||
Debug bool `toml:"debug" json:"debug,omitempty"`
|
||||
Secret string `toml:"secret" json:"secret"`
|
||||
BindTo string `toml:"bind-to" json:"bind-to"`
|
||||
TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"`
|
||||
PreferIP string `toml:"prefer-ip" json:"prefer-ip,omitempty"`
|
||||
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domain-fronting-port,omitempty"`
|
||||
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerate-time-skewness,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:"max-size,omitempty"`
|
||||
ErrorRate float64 `toml:"error-rate" json:"error-rate,omitempty"`
|
||||
} `toml:"anti-replay" json:"anti-replay,omitempty"`
|
||||
Blocklist struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
DownloadConcurrency uint `toml:"download-concurrency" json:"download-concurrency,omitempty"`
|
||||
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||
UpdateEach string `toml:"update-each" json:"update-each,omitempty"`
|
||||
} `toml:"blocklist" json:"blocklist,omitempty"`
|
||||
} `toml:"defense" json:"defense,omitempty"`
|
||||
Network struct {
|
||||
Timeout struct {
|
||||
TCP string `toml:"tcp" json:"tcp,omitempty"`
|
||||
HTTP string `toml:"http" json:"http,omitempty"`
|
||||
Idle string `toml:"idle" json:"idle,omitempty"`
|
||||
} `toml:"timeout" json:"timeout,omitempty"`
|
||||
DOHIP string `toml:"doh-ip" json:"doh-ip,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:"metric-prefix,omitempty"`
|
||||
TagFormat string `toml:"tag-format" json:"tag-format,omitempty"`
|
||||
} `toml:"statsd" json:"statsd,omitempty"`
|
||||
Prometheus struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
BindTo string `toml:"bind-to" json:"bind-to,omitempty"`
|
||||
HTTPPath string `toml:"http-path" json:"http-path,omitempty"`
|
||||
MetricPrefix string `toml:"metric-prefix" json:"metric-prefix,omitempty"`
|
||||
} `toml:"prometheus" json:"prometheus,omitempty"`
|
||||
} `toml:"stats" json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
func Parse(rawData []byte) (*Config, error) {
|
||||
rawConf := &configRaw{}
|
||||
jsonBuf := &bytes.Buffer{}
|
||||
conf := &Config{}
|
||||
|
||||
jsonEncoder := json.NewEncoder(jsonBuf)
|
||||
jsonEncoder.SetEscapeHTML(false)
|
||||
jsonEncoder.SetIndent("", "")
|
||||
|
||||
if err := toml.Unmarshal(rawData, rawConf); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse toml config: %w", err)
|
||||
}
|
||||
|
||||
if err := jsonEncoder.Encode(rawConf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse a config: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("cannot validate config: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
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,68 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type TypeBlocklistURI struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *TypeBlocklistURI) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
text := string(data)
|
||||
if filepath.IsAbs(text) {
|
||||
if _, err := os.Stat(text); os.IsNotExist(err) {
|
||||
return fmt.Errorf("filepath %s does not exist", text)
|
||||
}
|
||||
|
||||
c.value = text
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect url: %w", err)
|
||||
}
|
||||
|
||||
switch parsedURL.Scheme {
|
||||
case "http", "https": // nolint: goconst
|
||||
default:
|
||||
return fmt.Errorf("unknown schema %s", parsedURL.Scheme)
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("incorrect url %s", text)
|
||||
}
|
||||
|
||||
c.value = parsedURL.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeBlocklistURI) MarshalText() ([]byte, error) {
|
||||
return []byte(c.value), nil
|
||||
}
|
||||
|
||||
func (c TypeBlocklistURI) String() string {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func (c TypeBlocklistURI) IsRemote() bool {
|
||||
return !filepath.IsAbs(c.value)
|
||||
}
|
||||
|
||||
func (c TypeBlocklistURI) Value(defaultValue string) string {
|
||||
if c.value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"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
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnmarshalNil() {
|
||||
typ := &config.TypeBlocklistURI{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.Empty(typ.String())
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnknownSchema() {
|
||||
typ := &config.TypeBlocklistURI{}
|
||||
suite.Error(typ.UnmarshalText([]byte("gopher://lalala")))
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestEmptyHost() {
|
||||
typ := &config.TypeBlocklistURI{}
|
||||
suite.Error(typ.UnmarshalText([]byte("https:///path")))
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestIncorrectURL() {
|
||||
typ := &config.TypeBlocklistURI{}
|
||||
suite.Error(typ.UnmarshalText([]byte("h:/--")))
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() {
|
||||
rnd := make([]byte, 48)
|
||||
|
||||
rand.Read(rnd) // nolint: errcheck
|
||||
|
||||
unknownPath := base64.StdEncoding.EncodeToString(rnd)
|
||||
|
||||
testData := []string{
|
||||
"1",
|
||||
unknownPath,
|
||||
"/" + unknownPath,
|
||||
"http:/",
|
||||
"gopher://lalalal",
|
||||
}
|
||||
|
||||
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() {
|
||||
dir, _ := os.Getwd()
|
||||
dir, _ = filepath.Abs(dir)
|
||||
|
||||
testData := []string{
|
||||
"http://lalala",
|
||||
filepath.Join(dir, "config.go"),
|
||||
"https://lalala",
|
||||
}
|
||||
|
||||
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.Value(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestMarshalOk() {
|
||||
dir, _ := os.Getwd()
|
||||
dir, _ = filepath.Abs(dir)
|
||||
|
||||
testData := []string{
|
||||
"http://lalalal",
|
||||
filepath.Join(dir, "config.go"),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
name := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": name,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
testStruct := &typeBlocklistURITestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, name, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, name, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestValue() {
|
||||
testStruct := &typeBlocklistURITestStruct{}
|
||||
|
||||
suite.Equal("http://lalala", testStruct.Value.Value("http://lalala"))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "http://blablabla",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.Equal("http://blablabla", testStruct.Value.Value(""))
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestIsRemote() {
|
||||
dir, _ := os.Getwd()
|
||||
dir, _ = filepath.Abs(dir)
|
||||
|
||||
testData := map[bool]string{
|
||||
true: "http://lalalal",
|
||||
false: filepath.Join(dir, "config.go"),
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
ok := k
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.FormatBool(ok), func(t *testing.T) {
|
||||
testStruct := &typeBlocklistURITestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
|
||||
if ok {
|
||||
assert.True(t, testStruct.Value.IsRemote())
|
||||
} else {
|
||||
assert.False(t, testStruct.Value.IsRemote())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeBlocklistURI(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBlocklistURITestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
)
|
||||
|
||||
type TypeBytes struct {
|
||||
value units.Base2Bytes
|
||||
}
|
||||
|
||||
func (c *TypeBytes) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
normalizedData := strings.ToUpper(string(data))
|
||||
normalizedData = strings.ReplaceAll(normalizedData, "IB", "iB")
|
||||
|
||||
value, err := units.ParseBase2Bytes(normalizedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect bytes value: %w", err)
|
||||
}
|
||||
|
||||
if value < 0 {
|
||||
return fmt.Errorf("%d should be positive number", value)
|
||||
}
|
||||
|
||||
c.value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeBytes) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeBytes) String() string {
|
||||
if c.value == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(c.value.String())
|
||||
}
|
||||
|
||||
func (c TypeBytes) Value(defaultValue uint) uint {
|
||||
if c.value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return uint(c.value)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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) TestUnmarshalNil() {
|
||||
typ := &config.TypeBytes{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.Empty(typ.String())
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"1m",
|
||||
"1",
|
||||
"-1kb",
|
||||
"-1kib",
|
||||
"-1QB",
|
||||
}
|
||||
|
||||
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.Value(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"1b",
|
||||
"1kib",
|
||||
"2mib",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
name := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": name,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
testStruct := &typeBytesTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, name, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, name, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestValue() {
|
||||
testStruct := &typeBytesTestStruct{}
|
||||
|
||||
suite.EqualValues(0, testStruct.Value.Value(0))
|
||||
suite.EqualValues(1, testStruct.Value.Value(1))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "1kb",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.EqualValues(1024, testStruct.Value.Value(0))
|
||||
suite.EqualValues(1024, testStruct.Value.Value(1))
|
||||
}
|
||||
|
||||
func TestTypeBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBytesTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TypeDuration struct {
|
||||
value time.Duration
|
||||
}
|
||||
|
||||
func (c *TypeDuration) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dur, err := time.ParseDuration(strings.ToLower(string(data)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect duration: %w", err)
|
||||
}
|
||||
|
||||
if dur < 0 {
|
||||
return fmt.Errorf("%s should be positive duration", dur)
|
||||
}
|
||||
|
||||
c.value = dur
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeDuration) MarshalText() ([]byte, error) {
|
||||
return []byte(c.value.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeDuration) String() string {
|
||||
return c.value.String()
|
||||
}
|
||||
|
||||
func (c TypeDuration) Value(defaultValue time.Duration) time.Duration {
|
||||
if c.value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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) TestUnmarshalNil() {
|
||||
typ := &config.TypeDuration{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.EqualValues(0, typ.Value(0))
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"1t",
|
||||
"1",
|
||||
"-1s",
|
||||
"-1h",
|
||||
}
|
||||
|
||||
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,
|
||||
"1m": time.Minute,
|
||||
"2h1s": 2*time.Hour + time.Second,
|
||||
}
|
||||
|
||||
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(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"1s",
|
||||
"1m0s",
|
||||
"2h0m1s",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
name := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": name,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
testStruct := &typeDurationTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, name, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, name, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestValue() {
|
||||
testStruct := &typeDurationTestStruct{}
|
||||
|
||||
suite.EqualValues(0, testStruct.Value.Value(0))
|
||||
suite.Equal(time.Second, testStruct.Value.Value(time.Second))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "1s",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.Equal(time.Second, testStruct.Value.Value(0))
|
||||
suite.Equal(time.Second, testStruct.Value.Value(time.Minute))
|
||||
}
|
||||
|
||||
func TestTypeDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeDurationTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const typeErrorRateIgnoreLess = 1e-8
|
||||
|
||||
type TypeErrorRate struct {
|
||||
value float64
|
||||
}
|
||||
|
||||
func (c *TypeErrorRate) UnmarshalJSON(data []byte) error {
|
||||
value, err := strconv.ParseFloat(string(data), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect float value: %w", err)
|
||||
}
|
||||
|
||||
if value <= 0 || value >= 100 {
|
||||
return fmt.Errorf("%f should be 0 < x < 100", value)
|
||||
}
|
||||
|
||||
c.value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypeErrorRate) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeErrorRate) String() string {
|
||||
return strconv.FormatFloat(c.value, 'f', -1, 64)
|
||||
}
|
||||
|
||||
func (c TypeErrorRate) Value(defaultValue float64) float64 {
|
||||
if c.value < typeErrorRateIgnoreLess {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
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 typeErrorRateTestStruct struct {
|
||||
Value config.TypeErrorRate `json:"value"`
|
||||
}
|
||||
|
||||
type TypeErrorRateTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() {
|
||||
testData := []float64{
|
||||
1000,
|
||||
-100,
|
||||
-0.0001,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]float64{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{}))
|
||||
})
|
||||
}
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "hello",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.Error(json.Unmarshal(data, &typeErrorRateTestStruct{}))
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() {
|
||||
testData := []float64{
|
||||
1,
|
||||
55.5,
|
||||
0.0001,
|
||||
1e-6,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]float64{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
|
||||
testStruct := &typeErrorRateTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.InEpsilon(t, value, testStruct.Value.Value(0), 1e-10)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestMarshalOk() {
|
||||
testData := []float64{
|
||||
1,
|
||||
55.5,
|
||||
0.0001,
|
||||
1e-6,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]float64{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
|
||||
testStruct := &typeErrorRateTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
|
||||
parsed, err := strconv.ParseFloat(testStruct.Value.String(), 64)
|
||||
assert.NoError(t, err)
|
||||
assert.InEpsilon(t, value, parsed, 1e-10)
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
|
||||
parsed, err = strconv.ParseFloat(string(marshalled), 64)
|
||||
assert.NoError(t, err)
|
||||
assert.InEpsilon(t, value, parsed, 1e-10)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestValue() {
|
||||
testStruct := &typeErrorRateTestStruct{}
|
||||
|
||||
suite.InEpsilon(1, testStruct.Value.Value(1), 1e-10)
|
||||
suite.InEpsilon(2, testStruct.Value.Value(2), 1e-10)
|
||||
|
||||
data, err := json.Marshal(map[string]float64{
|
||||
"value": 1,
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.InEpsilon(1, testStruct.Value.Value(2), 1e-10)
|
||||
suite.InEpsilon(1, testStruct.Value.Value(3), 1e-10)
|
||||
}
|
||||
|
||||
func TestTypeErrorRate(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeErrorRateTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeHostPort struct {
|
||||
host TypeIP
|
||||
port TypePort
|
||||
}
|
||||
|
||||
func (c *TypeHostPort) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
text := string(data)
|
||||
|
||||
host, port, err := net.SplitHostPort(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect host:port syntax: %w", err)
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
return fmt.Errorf("port in %s host:port pair cannot be empty", text)
|
||||
}
|
||||
|
||||
if err := c.port.UnmarshalJSON([]byte(port)); err != nil {
|
||||
return fmt.Errorf("incorrect port in host:port: %w", err)
|
||||
}
|
||||
|
||||
if err := c.host.UnmarshalText([]byte(host)); err != nil {
|
||||
return fmt.Errorf("incorrect host: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeHostPort) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeHostPort) String() string {
|
||||
return c.Value(net.IP{}, 0)
|
||||
}
|
||||
|
||||
func (c TypeHostPort) HostValue(defaultValue net.IP) net.IP {
|
||||
return c.host.Value(defaultValue)
|
||||
}
|
||||
|
||||
func (c TypeHostPort) PortValue(defaultValue uint) uint {
|
||||
return c.port.Value(defaultValue)
|
||||
}
|
||||
|
||||
func (c TypeHostPort) Value(defaultHostValue net.IP, defaultPortValue uint) string {
|
||||
host := c.HostValue(defaultHostValue)
|
||||
port := c.PortValue(defaultPortValue)
|
||||
|
||||
hostStr := ""
|
||||
if len(host) > 0 {
|
||||
hostStr = host.String()
|
||||
}
|
||||
|
||||
return net.JoinHostPort(hostStr, strconv.Itoa(int(port)))
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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 typeHostPortTestStruct struct {
|
||||
Value config.TypeHostPort `json:"value"`
|
||||
}
|
||||
|
||||
type TypeHostPortTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"10.0.0.10:aaa",
|
||||
"10.0.0.10:",
|
||||
":",
|
||||
"xxx",
|
||||
"xxx:80",
|
||||
}
|
||||
|
||||
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{
|
||||
"10.0.0.10:80",
|
||||
"0.0.0.0:80",
|
||||
":8000",
|
||||
}
|
||||
|
||||
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.EqualValues(t, value, testStruct.Value.Value(nil, 0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"10.0.0.10:80",
|
||||
"0.0.0.0:80",
|
||||
":8000",
|
||||
}
|
||||
|
||||
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.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestValue() {
|
||||
testStruct := &typeHostPortTestStruct{}
|
||||
|
||||
suite.EqualValues("127.0.0.1:80",
|
||||
testStruct.Value.Value(net.ParseIP("127.0.0.1"), 80))
|
||||
suite.EqualValues("127.1.0.1:80",
|
||||
testStruct.Value.Value(net.ParseIP("127.1.0.1"), 80))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "127.0.0.1:80",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(nil, 0))
|
||||
suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(net.ParseIP("10.0.0.10"), 3000))
|
||||
}
|
||||
|
||||
func TestTypeHostPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeHostPortTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
type TypeHTTPPath struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *TypeHTTPPath) UnmarshalText(data []byte) error {
|
||||
if len(data) > 0 {
|
||||
c.value = "/" + strings.Trim(string(data), "/")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeHTTPPath) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeHTTPPath) String() string {
|
||||
if c.value == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
|
||||
func (c TypeHTTPPath) Value(defaultValue string) string {
|
||||
if c.value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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) TestUnmarshal() {
|
||||
testData := []string{
|
||||
"/hello",
|
||||
"hello",
|
||||
"hello/",
|
||||
"/hello/",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeHTTPPathTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, "/hello", testStruct.Value.Value(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestMarshalOk() {
|
||||
testData := map[string]string{
|
||||
"": "/",
|
||||
"/hello": "/hello",
|
||||
"/hello/": "/hello",
|
||||
"hello/": "/hello",
|
||||
"hello": "/hello",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
toPass := k
|
||||
compareWith := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": toPass,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(toPass, func(t *testing.T) {
|
||||
testStruct := &typeHTTPPathTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, compareWith, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, compareWith, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestValue() {
|
||||
testStruct := &typeHTTPPathTestStruct{}
|
||||
|
||||
suite.Equal("/hello", testStruct.Value.Value("/hello"))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "/map",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.Equal("/map", testStruct.Value.Value("/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 (c *TypeIP) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ip := net.ParseIP(string(data))
|
||||
if ip == nil {
|
||||
return fmt.Errorf("incorrect ip address: %s", string(data))
|
||||
}
|
||||
|
||||
c.value = ip
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypeIP) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeIP) String() string {
|
||||
if len(c.value) > 0 {
|
||||
return c.value.String()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c TypeIP) Value(defaultValue net.IP) net.IP {
|
||||
if c.value == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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.0.10",
|
||||
"10.0.0.10:",
|
||||
"xxx:80",
|
||||
"2001:0db8:85a3:0000:0000:8a2e:4",
|
||||
}
|
||||
|
||||
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 := []string{
|
||||
"0.0.0.0",
|
||||
"10.0.0.10",
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
}
|
||||
|
||||
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 := &typeIPTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t,
|
||||
net.ParseIP(value).String(),
|
||||
testStruct.Value.Value(nil).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"0.0.0.0",
|
||||
"10.0.0.10",
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := net.ParseIP(v).String()
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeIPTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestValue() {
|
||||
testStruct := &typeIPTestStruct{}
|
||||
suite.Empty(testStruct.Value.String())
|
||||
|
||||
suite.Nil(testStruct.Value.Value(nil))
|
||||
suite.Equal("127.1.0.1", testStruct.Value.Value(net.ParseIP("127.1.0.1")).String())
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "127.0.0.1",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.Equal("127.0.0.1", testStruct.Value.Value(nil).String())
|
||||
suite.Equal("127.0.0.1", testStruct.Value.Value(net.ParseIP("10.0.0.10")).String())
|
||||
}
|
||||
|
||||
func TestTypeIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeIPTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type TypeMetricPrefix struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *TypeMetricPrefix) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix := string(data)
|
||||
if ok, err := regexp.MatchString("^[a-z0-9]+$", prefix); !ok || err != nil {
|
||||
return fmt.Errorf("incorrect metric prefix: %s", prefix)
|
||||
}
|
||||
|
||||
c.value = prefix
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeMetricPrefix) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeMetricPrefix) String() string {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func (c TypeMetricPrefix) Value(defaultValue string) string {
|
||||
if c.value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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) TestUnmarshalNil() {
|
||||
typ := &config.TypeMetricPrefix{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.Empty(typ.String())
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"aaa.aaa",
|
||||
"aaa-bbb",
|
||||
"aaa:ccc",
|
||||
"metric prefix",
|
||||
}
|
||||
|
||||
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() {
|
||||
testData := []string{
|
||||
"mtg",
|
||||
"mtg111",
|
||||
}
|
||||
|
||||
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 := &typeMetricPrefixTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Value(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"mtg",
|
||||
"mtg111",
|
||||
}
|
||||
|
||||
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 := &typeMetricPrefixTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestValue() {
|
||||
testStruct := &typeMetricPrefixTestStruct{}
|
||||
|
||||
suite.Equal("mtg", testStruct.Value.Value("mtg"))
|
||||
suite.Equal("vvv", testStruct.Value.Value("vvv"))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "aaa",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.Equal("aaa", testStruct.Value.Value("mtg"))
|
||||
suite.Equal("aaa", testStruct.Value.Value("vvv"))
|
||||
}
|
||||
|
||||
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 (c *TypePort) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
intValue, err := strconv.ParseUint(string(data), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port number is not a number: %w", err)
|
||||
}
|
||||
|
||||
if intValue == 0 || intValue >= 65536 {
|
||||
return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue)
|
||||
}
|
||||
|
||||
c.value = uint(intValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypePort) MarshalJSON() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypePort) String() string {
|
||||
return strconv.Itoa(int(c.value))
|
||||
}
|
||||
|
||||
func (c TypePort) Value(defaultValue uint) uint {
|
||||
if c.value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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 typePortTestStruct struct {
|
||||
Value config.TypePort `json:"value"`
|
||||
}
|
||||
|
||||
type TypePortTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalNil() {
|
||||
typ := &config.TypePort{}
|
||||
suite.NoError(typ.UnmarshalJSON(nil))
|
||||
suite.Equal("0", typ.String())
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalFail() {
|
||||
testData := []int{
|
||||
-1,
|
||||
1_000_000,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]int{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.Itoa(v), func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typePortTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalOk() {
|
||||
testData := []int{
|
||||
1,
|
||||
1_000,
|
||||
65535,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]int{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.Itoa(v), func(t *testing.T) {
|
||||
testStruct := &typePortTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.EqualValues(t, value, testStruct.Value.Value(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestMarshalOk() {
|
||||
testData := map[string]int{
|
||||
"1": 1,
|
||||
"1000": 1000,
|
||||
"65535": 65535,
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
name := k
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]int{
|
||||
"value": value,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
testStruct := &typePortTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, name, testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalJSON()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, name, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestValue() {
|
||||
testStruct := &typePortTestStruct{}
|
||||
|
||||
suite.EqualValues(0, testStruct.Value.Value(0))
|
||||
suite.EqualValues(1, testStruct.Value.Value(1))
|
||||
|
||||
data, err := json.Marshal(map[string]int{
|
||||
"value": 5,
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.EqualValues(5, testStruct.Value.Value(0))
|
||||
suite.EqualValues(5, testStruct.Value.Value(1))
|
||||
}
|
||||
|
||||
func TestTypePort(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypePortTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TypePreferIPPreferIPv4 = "prefer-ipv4"
|
||||
TypePreferIPPreferIPv6 = "prefer-ipv6"
|
||||
TypePreferOnlyIPv4 = "only-ipv4"
|
||||
TypePreferOnlyIPv6 = "only-ipv6"
|
||||
)
|
||||
|
||||
type TypePreferIP struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *TypePreferIP) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
text := strings.ToLower(string(data))
|
||||
|
||||
switch text {
|
||||
case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, TypePreferOnlyIPv4, TypePreferOnlyIPv6:
|
||||
c.value = text
|
||||
default:
|
||||
return fmt.Errorf("incorrect prefer-ip value: %s", string(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypePreferIP) MarshalText() ([]byte, error) {
|
||||
return []byte(c.value), nil
|
||||
}
|
||||
|
||||
func (c *TypePreferIP) String() string {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func (c *TypePreferIP) Value(defaultValue string) string {
|
||||
if c.value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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) TestUnmarshalNil() {
|
||||
typ := &config.TypePreferIP{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.Empty(typ.String())
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"p",
|
||||
"ipv4",
|
||||
"onlyipv4",
|
||||
"ipv6prefer",
|
||||
}
|
||||
|
||||
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.ToUpper(config.TypePreferIPPreferIPv4),
|
||||
strings.ToUpper(config.TypePreferIPPreferIPv6),
|
||||
strings.ToUpper(config.TypePreferOnlyIPv4),
|
||||
strings.ToUpper(config.TypePreferOnlyIPv6),
|
||||
strings.ToLower(config.TypePreferIPPreferIPv4),
|
||||
strings.ToLower(config.TypePreferIPPreferIPv6),
|
||||
strings.ToLower(config.TypePreferOnlyIPv4),
|
||||
strings.ToLower(config.TypePreferOnlyIPv6),
|
||||
}
|
||||
|
||||
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.EqualValues(t,
|
||||
strings.ToLower(value),
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv4))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
config.TypePreferIPPreferIPv4,
|
||||
config.TypePreferIPPreferIPv6,
|
||||
config.TypePreferOnlyIPv4,
|
||||
config.TypePreferOnlyIPv6,
|
||||
strings.ToUpper(config.TypePreferIPPreferIPv4),
|
||||
strings.ToUpper(config.TypePreferIPPreferIPv6),
|
||||
strings.ToUpper(config.TypePreferOnlyIPv4),
|
||||
strings.ToUpper(config.TypePreferOnlyIPv6),
|
||||
strings.ToLower(config.TypePreferIPPreferIPv4),
|
||||
strings.ToLower(config.TypePreferIPPreferIPv6),
|
||||
strings.ToLower(config.TypePreferOnlyIPv4),
|
||||
strings.ToLower(config.TypePreferOnlyIPv6),
|
||||
}
|
||||
|
||||
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.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ToLower(value), string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestValue() {
|
||||
testStruct := &typePreferIPTestStruct{}
|
||||
|
||||
suite.EqualValues(config.TypePreferIPPreferIPv4,
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv4))
|
||||
suite.EqualValues(config.TypePreferIPPreferIPv6,
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": config.TypePreferOnlyIPv4,
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.EqualValues(config.TypePreferOnlyIPv4,
|
||||
testStruct.Value.Value(config.TypePreferOnlyIPv6))
|
||||
suite.EqualValues(config.TypePreferOnlyIPv4,
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
|
||||
}
|
||||
|
||||
func TestTypePreferIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypePreferIPTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeStatsdTagFormatInfluxdb = "influxdb"
|
||||
TypeStatsdTagFormatDatadog = "datadog"
|
||||
TypeStatsdTagFormatGraphite = "graphite"
|
||||
)
|
||||
|
||||
type TypeStatsdTagFormat struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *TypeStatsdTagFormat) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
text := strings.ToLower(string(data))
|
||||
|
||||
switch text {
|
||||
case TypeStatsdTagFormatInfluxdb, TypeStatsdTagFormatDatadog, TypeStatsdTagFormatGraphite:
|
||||
c.value = text
|
||||
default:
|
||||
return fmt.Errorf("incorrect tag format value: %s", string(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TypeStatsdTagFormat) MarshalText() ([]byte, error) {
|
||||
return []byte(c.value), nil
|
||||
}
|
||||
|
||||
func (c *TypeStatsdTagFormat) String() string {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func (c *TypeStatsdTagFormat) Value(defaultValue string) string {
|
||||
if c.value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeStatsdTagFormatTestStruct struct {
|
||||
Value config.TypeStatsdTagFormat `json:"value"`
|
||||
}
|
||||
|
||||
type TypeStatsdTagFormatTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalNil() {
|
||||
typ := &config.TypeStatsdTagFormat{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.Equal("lalala", typ.Value("lalala"))
|
||||
}
|
||||
|
||||
func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"p",
|
||||
"ipv4",
|
||||
"onlyipv4",
|
||||
"ipv6prefer",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
config.TypeStatsdTagFormatDatadog,
|
||||
config.TypeStatsdTagFormatInfluxdb,
|
||||
config.TypeStatsdTagFormatGraphite,
|
||||
strings.ToUpper(config.TypeStatsdTagFormatDatadog),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatGraphite),
|
||||
strings.ToLower(config.TypeStatsdTagFormatDatadog),
|
||||
strings.ToLower(config.TypeStatsdTagFormatInfluxdb),
|
||||
strings.ToLower(config.TypeStatsdTagFormatGraphite),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeStatsdTagFormatTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.EqualValues(t,
|
||||
strings.ToLower(value),
|
||||
testStruct.Value.Value(config.TypeStatsdTagFormatDatadog))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeStatsdTagFormatTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
config.TypeStatsdTagFormatDatadog,
|
||||
config.TypeStatsdTagFormatInfluxdb,
|
||||
config.TypeStatsdTagFormatGraphite,
|
||||
strings.ToUpper(config.TypeStatsdTagFormatDatadog),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatGraphite),
|
||||
strings.ToLower(config.TypeStatsdTagFormatDatadog),
|
||||
strings.ToLower(config.TypeStatsdTagFormatInfluxdb),
|
||||
strings.ToLower(config.TypeStatsdTagFormatGraphite),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeStatsdTagFormatTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, strings.ToLower(value), testStruct.Value.String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ToLower(value), string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeStatsdTagFormatTestSuite) TestValue() {
|
||||
testStruct := &typePreferIPTestStruct{}
|
||||
|
||||
suite.EqualValues(config.TypePreferIPPreferIPv4,
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv4))
|
||||
suite.EqualValues(config.TypePreferIPPreferIPv6,
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": config.TypePreferOnlyIPv4,
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.EqualValues(config.TypePreferOnlyIPv4,
|
||||
testStruct.Value.Value(config.TypePreferOnlyIPv6))
|
||||
suite.EqualValues(config.TypePreferOnlyIPv4,
|
||||
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
|
||||
}
|
||||
|
||||
func TestTypeStatsdTagFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeStatsdTagFormatTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type TypeURL struct {
|
||||
value *url.URL
|
||||
}
|
||||
|
||||
func (c *TypeURL) UnmarshalText(data []byte) error { // nolint: cyclop
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, err := url.Parse(string(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect URL: %w", err)
|
||||
}
|
||||
|
||||
switch value.Scheme {
|
||||
case "http", "https", "socks5":
|
||||
case "":
|
||||
return fmt.Errorf("url %s has to have a schema", value)
|
||||
default:
|
||||
return fmt.Errorf("unsupported schema %s", value.Scheme)
|
||||
}
|
||||
|
||||
if value.Host == "" {
|
||||
return fmt.Errorf("url %s has to have a host", value)
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(value.Host); err != nil {
|
||||
switch value.Scheme {
|
||||
case "http":
|
||||
value.Host = net.JoinHostPort(value.Host, "80")
|
||||
case "https":
|
||||
value.Host = net.JoinHostPort(value.Host, "443")
|
||||
case "socks5":
|
||||
value.Host = net.JoinHostPort(value.Host, "1080")
|
||||
default:
|
||||
return fmt.Errorf("cannot set a default port for %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
c.value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TypeURL) MarshalText() ([]byte, error) {
|
||||
return []byte(c.String()), nil
|
||||
}
|
||||
|
||||
func (c TypeURL) String() string {
|
||||
if c.value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.value.String()
|
||||
}
|
||||
|
||||
func (c TypeURL) Value(defaultValue *url.URL) *url.URL {
|
||||
if c.value == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return c.value
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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 typeURLTestStruct struct {
|
||||
Value config.TypeURL `json:"value"`
|
||||
}
|
||||
|
||||
type TypeURLTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeURLTestSuite) TestUnmarshalNil() {
|
||||
u, _ := url.Parse("https://google.com")
|
||||
|
||||
typ := &config.TypeURL{}
|
||||
suite.NoError(typ.UnmarshalText(nil))
|
||||
suite.Empty(typ.String())
|
||||
suite.Equal("https://google.com", typ.Value(u).String())
|
||||
}
|
||||
|
||||
func (suite *TypeURLTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"http:/aaa.com",
|
||||
"ipv4",
|
||||
"111",
|
||||
"://111",
|
||||
"http://aaa.com:xxx",
|
||||
"gopher://aaa.com:888",
|
||||
"gopher://aaa.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, &typeURLTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeURLTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"https://10.0.0.10:80": "https://10.0.0.10:80",
|
||||
"https://10.0.0.10:443": "https://10.0.0.10",
|
||||
"http://10.0.0.10:8": "http://10.0.0.10:8",
|
||||
"http://10.0.0.10:80": "http://10.0.0.10",
|
||||
"socks5://10.0.0.10:1080": "socks5://10.0.0.10",
|
||||
"socks5://10.0.0.10:888": "socks5://10.0.0.10:888",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
expected := k
|
||||
actual := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": actual,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(actual, func(t *testing.T) {
|
||||
testStruct := &typeURLTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, expected, testStruct.Value.Value(nil).String())
|
||||
|
||||
marshalled, err := testStruct.Value.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(marshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeURLTestSuite) TestValue() {
|
||||
testStruct := &typeURLTestStruct{}
|
||||
|
||||
u1, _ := url.Parse("https://10.0.0.10:80")
|
||||
u2, _ := url.Parse("https://10.1.0.10:80")
|
||||
|
||||
suite.Equal("https://10.0.0.10:80", testStruct.Value.Value(u1).String())
|
||||
suite.Equal("https://10.1.0.10:80", testStruct.Value.Value(u2).String())
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": "http://127.0.0.1:80",
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
|
||||
suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u1).String())
|
||||
suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u2).String())
|
||||
}
|
||||
|
||||
func TestTypeURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeURLTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CaptureStdout(callback func()) string {
|
||||
return captureOutput(&os.Stdout, callback)
|
||||
}
|
||||
|
||||
func CaptureStderr(callback func()) string {
|
||||
return captureOutput(&os.Stderr, callback)
|
||||
}
|
||||
|
||||
func captureOutput(filefp **os.File, callback func()) string {
|
||||
oldFp := *filefp
|
||||
|
||||
defer func() {
|
||||
*filefp = oldFp
|
||||
}()
|
||||
|
||||
reader, writer, _ := os.Pipe()
|
||||
buf := &bytes.Buffer{}
|
||||
closeChan := make(chan bool)
|
||||
|
||||
go func() {
|
||||
io.Copy(buf, reader) // nolint: errcheck
|
||||
close(closeChan)
|
||||
}()
|
||||
|
||||
*filefp = writer
|
||||
|
||||
callback()
|
||||
|
||||
writer.Close()
|
||||
<-closeChan
|
||||
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package testlib
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type EventsObserverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package testlib
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type MtglibAntiReplayCacheMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MtglibAntiReplayCacheMock) SeenBefore(data []byte) bool {
|
||||
return m.Called(data).Bool(0)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MtglibNetworkMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) Dial(network, address string) (net.Conn, error) {
|
||||
args := m.Called(network, address)
|
||||
|
||||
return args.Get(0).(net.Conn), args.Error(1) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
args := m.Called(ctx, network, address)
|
||||
|
||||
return args.Get(0).(net.Conn), args.Error(1) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
|
||||
network, address string) (net.Conn, error)) *http.Client {
|
||||
return m.Called(dialFunc).Get(0).(*http.Client)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type NetConnMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (n *NetConnMock) Read(b []byte) (int, error) {
|
||||
args := n.Called(b)
|
||||
|
||||
return args.Int(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) Write(b []byte) (int, error) {
|
||||
args := n.Called(b)
|
||||
|
||||
return args.Int(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) Close() error {
|
||||
return n.Called().Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *NetConnMock) LocalAddr() net.Addr {
|
||||
return n.Called().Get(0).(net.Addr)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) RemoteAddr() net.Addr {
|
||||
return n.Called().Get(0).(net.Addr)
|
||||
}
|
||||
|
||||
func (n *NetConnMock) SetDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *NetConnMock) SetReadDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *NetConnMock) SetWriteDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func SetLimits() error {
|
||||
rLimit := unix.Rlimit{}
|
||||
if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
return fmt.Errorf("cannot get rlimit: %w", err)
|
||||
}
|
||||
|
||||
rLimit.Cur = rLimit.Max
|
||||
|
||||
if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
return fmt.Errorf("cannot set rlimit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
func SetLimits() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func RootContext() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
for range sigChan {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
func RootContext() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
go func() {
|
||||
for range sigChan {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
package ipblocklist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/kentik/patricia"
|
||||
"github.com/kentik/patricia/bool_tree"
|
||||
"github.com/panjf2000/ants/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
fireholIPv4DefaultCIDR = 32
|
||||
fireholIPv6DefaultCIDR = 128
|
||||
)
|
||||
|
||||
var fireholRegexpComment = regexp.MustCompile(`\s*#.*?$`)
|
||||
|
||||
// Firehol is IPBlocklist which uses lists from FireHOL:
|
||||
// https://iplists.firehol.org/
|
||||
//
|
||||
// It can use both local files and remote URLs. This is not necessary
|
||||
// that blocklists should be taken from this website, we expect only
|
||||
// compatible formats here.
|
||||
//
|
||||
// Example of the format:
|
||||
//
|
||||
// # this is a comment
|
||||
// # to ignore
|
||||
// 127.0.0.1 # you can specify an IP
|
||||
// 10.0.0.0/8 # or cidr
|
||||
type Firehol struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
logger mtglib.Logger
|
||||
|
||||
rwMutex sync.RWMutex
|
||||
|
||||
remoteURLs []string
|
||||
localFiles []string
|
||||
|
||||
httpClient *http.Client
|
||||
workerPool *ants.Pool
|
||||
|
||||
treeV4 *bool_tree.TreeV4
|
||||
treeV6 *bool_tree.TreeV6
|
||||
}
|
||||
|
||||
// Shutdown stop a background update process.
|
||||
func (f *Firehol) Shutdown() {
|
||||
f.ctxCancel()
|
||||
}
|
||||
|
||||
// Contains is given IP list can be found in FireHOL blocklists.
|
||||
func (f *Firehol) Contains(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
f.rwMutex.RLock()
|
||||
defer f.rwMutex.RUnlock()
|
||||
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return f.containsIPv4(ip4)
|
||||
}
|
||||
|
||||
return f.containsIPv6(ip.To16())
|
||||
}
|
||||
|
||||
// Run starts a background update process.
|
||||
//
|
||||
// This is a blocking method so you probably want to run it in a
|
||||
// goroutine.
|
||||
func (f *Firehol) Run(updateEach time.Duration) {
|
||||
if updateEach == 0 {
|
||||
updateEach = DefaultFireholUpdateEach
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(updateEach)
|
||||
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
if err := f.update(); err != nil {
|
||||
f.logger.WarningError("cannot update blocklist", err)
|
||||
} else {
|
||||
f.logger.Info("blocklist was updated")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := f.update(); err != nil {
|
||||
f.logger.WarningError("cannot update blocklist", err)
|
||||
} else {
|
||||
f.logger.Info("blocklist was updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Firehol) containsIPv4(addr net.IP) bool {
|
||||
ip := patricia.NewIPv4AddressFromBytes(addr, 32)
|
||||
|
||||
if ok, _, err := f.treeV4.FindDeepestTag(ip); ok && err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Firehol) containsIPv6(addr net.IP) bool {
|
||||
ip := patricia.NewIPv6Address(addr, 128)
|
||||
|
||||
if ok, _, err := f.treeV6.FindDeepestTag(ip); ok && err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Firehol) update() error { // nolint: funlen, cyclop
|
||||
ctx, cancel := context.WithCancel(f.ctx)
|
||||
defer cancel()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(f.remoteURLs) + len(f.localFiles))
|
||||
|
||||
treeMutex := &sync.Mutex{}
|
||||
v4tree := bool_tree.NewTreeV4()
|
||||
v6tree := bool_tree.NewTreeV6()
|
||||
|
||||
errorChan := make(chan error, 1)
|
||||
defer close(errorChan)
|
||||
|
||||
for _, v := range f.localFiles {
|
||||
go func(filename string) {
|
||||
defer wg.Done()
|
||||
|
||||
if err := f.updateLocalFile(ctx, filename, treeMutex, v4tree, v6tree); err != nil {
|
||||
cancel()
|
||||
f.logger.BindStr("filename", filename).WarningError("cannot update", err)
|
||||
|
||||
select {
|
||||
case errorChan <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}(v)
|
||||
}
|
||||
|
||||
for _, v := range f.remoteURLs {
|
||||
value := v
|
||||
|
||||
f.workerPool.Submit(func() { // nolint: errcheck
|
||||
defer wg.Done()
|
||||
|
||||
if err := f.updateRemoteURL(ctx, value, treeMutex, v4tree, v6tree); err != nil {
|
||||
cancel()
|
||||
f.logger.BindStr("url", value).WarningError("cannot update", err)
|
||||
|
||||
select {
|
||||
case errorChan <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errorChan:
|
||||
return fmt.Errorf("cannot update trees: %w", err)
|
||||
default:
|
||||
}
|
||||
|
||||
f.rwMutex.Lock()
|
||||
defer f.rwMutex.Unlock()
|
||||
|
||||
f.treeV4 = v4tree
|
||||
f.treeV6 = v6tree
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firehol) updateLocalFile(ctx context.Context, filename string,
|
||||
mutex sync.Locker,
|
||||
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
|
||||
filefp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open file: %w", err)
|
||||
}
|
||||
|
||||
go func(ctx context.Context, closer io.Closer) {
|
||||
<-ctx.Done()
|
||||
closer.Close()
|
||||
}(ctx, filefp)
|
||||
|
||||
defer filefp.Close()
|
||||
|
||||
return f.updateTrees(mutex, filefp, v4tree, v6tree)
|
||||
}
|
||||
|
||||
func (f *Firehol) updateRemoteURL(ctx context.Context, url string,
|
||||
mutex sync.Locker,
|
||||
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build a request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := f.httpClient.Do(req) // nolint: bodyclose
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot request a remote URL %s: %w", url, err)
|
||||
}
|
||||
|
||||
go func(ctx context.Context, closer io.Closer) {
|
||||
<-ctx.Done()
|
||||
closer.Close()
|
||||
}(ctx, resp.Body)
|
||||
|
||||
defer func(rc io.ReadCloser) {
|
||||
io.Copy(io.Discard, rc) // nolint: errcheck
|
||||
rc.Close()
|
||||
}(resp.Body)
|
||||
|
||||
return f.updateTrees(mutex, resp.Body, v4tree, v6tree)
|
||||
}
|
||||
|
||||
func (f *Firehol) updateTrees(mutex sync.Locker,
|
||||
reader io.Reader,
|
||||
v4tree *bool_tree.TreeV4,
|
||||
v6tree *bool_tree.TreeV6) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
text = fireholRegexpComment.ReplaceAllLiteralString(text, "")
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ip, cidr, err := f.updateParseLine(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse a line: %w", err)
|
||||
}
|
||||
|
||||
if err := f.updateAddToTrees(ip, cidr, mutex, v4tree, v6tree); err != nil {
|
||||
return fmt.Errorf("cannot add a node to the tree: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if scanner.Err() != nil {
|
||||
return fmt.Errorf("cannot parse a response: %w", scanner.Err())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firehol) updateParseLine(text string) (net.IP, uint, error) {
|
||||
_, ipnet, err := net.ParseCIDR(text)
|
||||
if err != nil {
|
||||
ipaddr := net.ParseIP(text)
|
||||
if ipaddr == nil {
|
||||
return nil, 0, fmt.Errorf("incorrect ip address %s", text)
|
||||
}
|
||||
|
||||
ip4 := ipaddr.To4()
|
||||
if ip4 != nil {
|
||||
return ip4, fireholIPv4DefaultCIDR, nil
|
||||
}
|
||||
|
||||
return ipaddr.To16(), fireholIPv6DefaultCIDR, nil
|
||||
}
|
||||
|
||||
ones, _ := ipnet.Mask.Size()
|
||||
|
||||
return ipnet.IP, uint(ones), nil
|
||||
}
|
||||
|
||||
func (f *Firehol) updateAddToTrees(ip net.IP, cidr uint,
|
||||
mutex sync.Locker,
|
||||
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if ip.To4() != nil {
|
||||
addr := patricia.NewIPv4AddressFromBytes(ip, cidr)
|
||||
|
||||
if _, _, err := v4tree.Set(addr, true); err != nil {
|
||||
return err // nolint: wrapcheck
|
||||
}
|
||||
} else {
|
||||
addr := patricia.NewIPv6Address(ip, cidr)
|
||||
|
||||
if _, _, err := v6tree.Set(addr, true); err != nil {
|
||||
return err // nolint: wrapcheck
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewFirehol creates a new instance of FireHOL IP blocklist.
|
||||
//
|
||||
// This method does not start an update process so please execute Run
|
||||
// when it is necessary.
|
||||
func NewFirehol(logger mtglib.Logger, network mtglib.Network,
|
||||
downloadConcurrency uint,
|
||||
remoteURLs []string,
|
||||
localFiles []string) (*Firehol, error) {
|
||||
for _, v := range remoteURLs {
|
||||
parsed, err := url.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("incorrect url %s: %w", v, err)
|
||||
}
|
||||
|
||||
switch parsed.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported url %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range localFiles {
|
||||
if stat, err := os.Stat(v); os.IsNotExist(err) || stat.IsDir() || stat.Mode().Perm()&0o400 == 0 {
|
||||
return nil, fmt.Errorf("%s is not a readable file", v)
|
||||
}
|
||||
}
|
||||
|
||||
if downloadConcurrency == 0 {
|
||||
downloadConcurrency = DefaultFireholDownloadConcurrency
|
||||
}
|
||||
|
||||
workerPool, _ := ants.NewPool(int(downloadConcurrency))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Firehol{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
logger: logger.Named("firehol"),
|
||||
httpClient: network.MakeHTTPClient(nil),
|
||||
treeV4: bool_tree.NewTreeV4(),
|
||||
treeV6: bool_tree.NewTreeV6(),
|
||||
workerPool: workerPool,
|
||||
remoteURLs: remoteURLs,
|
||||
localFiles: localFiles,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package ipblocklist_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/9seconds/mtg/v2/network"
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type FireholTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
networkMock *testlib.MtglibNetworkMock
|
||||
httpServer *httptest.Server
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) SetupSuite() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
filefp, err := os.Open(filepath.Join("testdata", "remote_ipset.ipset"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer filefp.Close()
|
||||
|
||||
io.Copy(w, filefp) // nolint: errcheck
|
||||
})
|
||||
|
||||
suite.httpServer = httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) SetupTest() {
|
||||
httpClient := &http.Client{}
|
||||
suite.networkMock = &testlib.MtglibNetworkMock{}
|
||||
|
||||
httpmock.ActivateNonDefault(httpClient)
|
||||
|
||||
suite.networkMock.
|
||||
On("MakeHTTPClient", mock.Anything).
|
||||
Maybe().
|
||||
Return(httpClient)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TearDownTest() {
|
||||
suite.networkMock.AssertExpectations(suite.T())
|
||||
httpmock.DeactivateAndReset()
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TearDownSuite() {
|
||||
suite.httpServer.Close()
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestLocalFail() {
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
suite.networkMock, 2,
|
||||
nil, []string{filepath.Join("testdata", "broken_ipset.ipset")})
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.False(blocklist.Contains(net.ParseIP("10.0.0.10")))
|
||||
suite.False(blocklist.Contains(net.ParseIP("127.0.0.1")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestLocalOk() {
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
suite.networkMock, 2,
|
||||
nil, []string{filepath.Join("testdata", "good_ipset.ipset")})
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.True(blocklist.Contains(net.ParseIP("10.0.0.10")))
|
||||
suite.False(blocklist.Contains(net.ParseIP("127.0.0.1")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestRemoteFail() {
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
suite.networkMock, 2,
|
||||
[]string{"https://google.com"}, nil)
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.False(blocklist.Contains(net.ParseIP("10.2.2.2")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (suite *FireholTestSuite) TestMixed() {
|
||||
dialer, _ := network.NewDefaultDialer(0, 0)
|
||||
ntw, _ := network.NewNetwork(dialer, "mtg", "1.1.1.1", 0)
|
||||
|
||||
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
|
||||
ntw, 2,
|
||||
[]string{
|
||||
suite.httpServer.URL,
|
||||
}, []string{
|
||||
filepath.Join("testdata", "good_ipset.ipset"),
|
||||
})
|
||||
|
||||
suite.NoError(err)
|
||||
|
||||
go blocklist.Run(time.Hour)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
suite.True(blocklist.Contains(net.ParseIP("10.2.2.2")))
|
||||
suite.True(blocklist.Contains(net.ParseIP("10.1.0.100")))
|
||||
|
||||
blocklist.Shutdown()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestFirehol(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &FireholTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Package ipblocklist contains default implementation of the
|
||||
// IPBlocklist for mtg.
|
||||
//
|
||||
// Please check documentation for mtglib.IPBlocklist interface to get an
|
||||
// idea of this abstraction.
|
||||
package ipblocklist
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
// DefaultFireholDownloadConcurrency defines a default max number of
|
||||
// concurrent downloads of ip blocklists for Firehol.
|
||||
DefaultFireholDownloadConcurrency = 1
|
||||
|
||||
// DefaultFireholUpdateEach defines a default time period when
|
||||
// Firehol requests updates of the blocklists.
|
||||
DefaultFireholUpdateEach = 6 * time.Hour
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package ipblocklist
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) Contains(ip net.IP) bool { return false }
|
||||
|
||||
// NewNoop returns a dummy ipblocklist which allows all incoming
|
||||
// connections.
|
||||
func NewNoop() mtglib.IPBlocklist {
|
||||
return noop{}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ipblocklist_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *NoopTestSuite) TestOp() {
|
||||
suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10")))
|
||||
suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10")))
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopTestSuite{})
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# This is an intentionally broken ipset.
|
||||
#
|
||||
|
||||
ajsdkfbd
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#
|
||||
# This is very good ipset
|
||||
#
|
||||
|
||||
10.0.0.10 # just an example
|
||||
10.1.0.0/24
|
||||
2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
+1
@@ -0,0 +1 @@
|
||||
10.2.2.2
|
||||
@@ -0,0 +1,14 @@
|
||||
// Package logger has implementation of loggers for mtglib.Logger
|
||||
// interface.
|
||||
//
|
||||
// Please see a description of that interface to get some agreements
|
||||
// which are used by mtglib.
|
||||
package logger
|
||||
|
||||
// StdLikeLogger is an interface which is close to log.Logger. This is
|
||||
// commonly used by many 3pp tools. While mtglib itself does not need
|
||||
// it, it is always a good idea to support it and have a transient end
|
||||
// to end logging.
|
||||
type StdLikeLogger interface {
|
||||
Printf(format string, args ...interface{})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package logger
|
||||
|
||||
import "github.com/9seconds/mtg/v2/mtglib"
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func (n noopLogger) Named(_ string) mtglib.Logger { return n }
|
||||
func (n noopLogger) BindInt(_ string, _ int) mtglib.Logger { return n }
|
||||
func (n noopLogger) BindStr(_, _ string) mtglib.Logger { return n }
|
||||
func (n noopLogger) Printf(_ string, _ ...interface{}) {}
|
||||
func (n noopLogger) Info(_ string) {}
|
||||
func (n noopLogger) Warning(_ string) {}
|
||||
func (n noopLogger) Debug(_ string) {}
|
||||
func (n noopLogger) InfoError(_ string, _ error) {}
|
||||
func (n noopLogger) WarningError(_ string, _ error) {}
|
||||
func (n noopLogger) DebugError(_ string, _ error) {}
|
||||
|
||||
// NewNoopLogger returns a logger which discards all events.
|
||||
func NewNoopLogger() mtglib.Logger {
|
||||
return noopLogger{}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type NoopLoggerTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *NoopLoggerTestSuite) TestLog() {
|
||||
suite.Empty(testlib.CaptureStdout(func() {
|
||||
suite.Empty(testlib.CaptureStderr(func() {
|
||||
log := logger.NewNoopLogger().Named("name")
|
||||
|
||||
log.BindInt("int", 1).BindStr("str", "1").Printf("info", 1, 2)
|
||||
log.BindInt("int", 1).BindStr("str", "1").Info("info")
|
||||
log.BindInt("int", 1).BindStr("str", "1").Warning("info")
|
||||
log.BindInt("int", 1).BindStr("str", "1").Debug("info")
|
||||
log.BindInt("int", 1).BindStr("str", "1").InfoError("info", io.EOF)
|
||||
log.BindInt("int", 1).BindStr("str", "1").WarningError("info", io.EOF)
|
||||
log.BindInt("int", 1).BindStr("str", "1").DebugError("info", io.EOF)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
func TestNoopLogger(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &NoopLoggerTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const loggerFieldName = "logger"
|
||||
|
||||
type zeroLogContextVarType uint8
|
||||
|
||||
const (
|
||||
zeroLogContextVarTypeUnknown zeroLogContextVarType = iota
|
||||
zeroLogContextVarTypeStr
|
||||
zeroLogContextVarTypeInt
|
||||
)
|
||||
|
||||
type zeroLogContext struct {
|
||||
name string
|
||||
log *zerolog.Logger
|
||||
|
||||
ctxVarType zeroLogContextVarType
|
||||
ctxVarName string
|
||||
ctxVarStr string
|
||||
ctxVarInt int
|
||||
|
||||
parent *zeroLogContext
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) Named(name string) mtglib.Logger {
|
||||
loggerName := z.name
|
||||
if loggerName == "" {
|
||||
loggerName = name
|
||||
} else {
|
||||
loggerName += "." + name
|
||||
}
|
||||
|
||||
return &zeroLogContext{
|
||||
name: loggerName,
|
||||
log: z.log,
|
||||
parent: z,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) BindInt(name string, value int) mtglib.Logger {
|
||||
return &zeroLogContext{
|
||||
name: z.name,
|
||||
log: z.log,
|
||||
ctxVarType: zeroLogContextVarTypeInt,
|
||||
ctxVarInt: value,
|
||||
ctxVarName: name,
|
||||
parent: z,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) BindStr(name, value string) mtglib.Logger {
|
||||
return &zeroLogContext{
|
||||
name: z.name,
|
||||
log: z.log,
|
||||
ctxVarType: zeroLogContextVarTypeStr,
|
||||
ctxVarStr: value,
|
||||
ctxVarName: name,
|
||||
parent: z,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) Printf(format string, args ...interface{}) {
|
||||
z.Debug(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) Info(msg string) {
|
||||
z.InfoError(msg, nil)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) Warning(msg string) {
|
||||
z.WarningError(msg, nil)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) Debug(msg string) {
|
||||
z.DebugError(msg, nil)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) InfoError(msg string, err error) {
|
||||
z.emitLog(z.log.Info(), msg, err)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) WarningError(msg string, err error) {
|
||||
z.emitLog(z.log.Warn(), msg, err)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) DebugError(msg string, err error) {
|
||||
z.emitLog(z.log.Debug(), msg, err)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) emitLog(evt *zerolog.Event, msg string, err error) {
|
||||
z.attachCtx(evt)
|
||||
|
||||
for current := z.parent; current != nil; current = current.parent {
|
||||
current.attachCtx(evt)
|
||||
}
|
||||
|
||||
evt.Str(loggerFieldName, z.name).Err(err).Msg(msg)
|
||||
}
|
||||
|
||||
func (z *zeroLogContext) attachCtx(evt *zerolog.Event) {
|
||||
switch z.ctxVarType {
|
||||
case zeroLogContextVarTypeStr:
|
||||
evt.Str(z.ctxVarName, z.ctxVarStr)
|
||||
case zeroLogContextVarTypeInt:
|
||||
evt.Int(z.ctxVarName, z.ctxVarInt)
|
||||
case zeroLogContextVarTypeUnknown:
|
||||
}
|
||||
}
|
||||
|
||||
// NewZeroLogger returns a logger which is using rs/zerolog library.
|
||||
func NewZeroLogger(log zerolog.Logger) mtglib.Logger {
|
||||
return &zeroLogContext{
|
||||
log: &log,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type zeroLoggerLogMessage struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
StrParam string `json:"strparam"`
|
||||
IntParam int `json:"intparam"`
|
||||
Logger string `json:"logger"`
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ZeroLoggerTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ZeroLoggerTestSuite) SetupSuite() {
|
||||
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFieldName = "level"
|
||||
}
|
||||
|
||||
func (suite *ZeroLoggerTestSuite) TestLog() {
|
||||
testData := map[string]func(mtglib.Logger){
|
||||
"info": func(l mtglib.Logger) { l.Info("hello") },
|
||||
"warn": func(l mtglib.Logger) { l.Warning("hello") },
|
||||
"printf": func(l mtglib.Logger) { l.Printf("hello") },
|
||||
"debug": func(l mtglib.Logger) { l.Debug("hello") },
|
||||
"info-error": func(l mtglib.Logger) { l.InfoError("hello", io.EOF) },
|
||||
"warn-error": func(l mtglib.Logger) { l.WarningError("hello", io.EOF) },
|
||||
"debug-error": func(l mtglib.Logger) { l.DebugError("hello", io.EOF) },
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
name := k
|
||||
callback := v
|
||||
level := strings.TrimSuffix(name, "-error")
|
||||
|
||||
suite.T().Run(name, func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
log := logger.NewZeroLogger(zerolog.New(buf).With().Timestamp().Logger())
|
||||
|
||||
callback(log.Named("name").BindInt("intparam", 1).BindStr("strparam", name))
|
||||
|
||||
msg := &zeroLoggerLogMessage{}
|
||||
assert.NoError(t, json.Unmarshal(buf.Bytes(), msg))
|
||||
|
||||
timestamp := time.Unix(msg.Timestamp/1000, (msg.Timestamp%1000)*1_000_000)
|
||||
assert.WithinDuration(t, time.Now(), timestamp, 100*time.Millisecond)
|
||||
|
||||
if level == "printf" {
|
||||
level = "debug"
|
||||
}
|
||||
|
||||
assert.Equal(t, level, msg.Level)
|
||||
assert.Equal(t, name, msg.StrParam)
|
||||
assert.EqualValues(t, 1, msg.IntParam)
|
||||
assert.Equal(t, "name", msg.Logger)
|
||||
assert.Equal(t, "hello", msg.Message)
|
||||
|
||||
if level != name && name != "printf" {
|
||||
assert.Equal(t, io.EOF.Error(), msg.Error)
|
||||
} else {
|
||||
assert.Empty(t, msg.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ZeroLoggerTestSuite) TestIndependence() {
|
||||
buf := &bytes.Buffer{}
|
||||
log := logger.NewZeroLogger(zerolog.New(buf).With().Timestamp().Logger())
|
||||
|
||||
log1 := log.Named("1")
|
||||
log2 := log.Named("2")
|
||||
log12 := log1.Named("2")
|
||||
|
||||
log1.BindInt("param", 1).Info("hello")
|
||||
|
||||
log1Output := buf.String()
|
||||
|
||||
buf.Reset()
|
||||
|
||||
log2.BindInt("lalala", 2).Info("hello")
|
||||
|
||||
log2Output := buf.String()
|
||||
|
||||
buf.Reset()
|
||||
|
||||
log12.BindStr("tttt", "qqq").Info("hello")
|
||||
|
||||
log12Output := buf.String()
|
||||
|
||||
suite.NotContains("lalala", log1Output)
|
||||
suite.NotContains("tttt", log1Output)
|
||||
suite.NotContains("param", log2Output)
|
||||
suite.NotContains("tttt", log1Output)
|
||||
suite.NotContains("param", log12Output)
|
||||
suite.NotContains("lalala", log12Output)
|
||||
}
|
||||
|
||||
func TestZeroLogger(t *testing.T) { // nolint: paralleltest
|
||||
suite.Run(t, &ZeroLoggerTestSuite{})
|
||||
}
|
||||
@@ -1,202 +1,35 @@
|
||||
// mtg is just a command-line application that starts a proxy.
|
||||
//
|
||||
// Application logic is how to read a config and configure mtglib.Proxy.
|
||||
// So, probably you need to read the documentation for mtglib package
|
||||
// first.
|
||||
//
|
||||
// mtglib is a core of the application. The rest of the packages provide
|
||||
// some default implementations for the interfaces, defined in mtglib.
|
||||
package main
|
||||
|
||||
//go:generate scripts/generate_version.sh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/juju/errors"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/ntp"
|
||||
"github.com/9seconds/mtg/proxy"
|
||||
"github.com/9seconds/mtg/stats"
|
||||
"github.com/9seconds/mtg/v2/internal/cli"
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
var (
|
||||
app = kingpin.New("mtg", "Simple MTPROTO proxy.")
|
||||
|
||||
debug = app.Flag("debug", "Run in debug mode.").
|
||||
Short('d').
|
||||
Envar("MTG_DEBUG").
|
||||
Bool()
|
||||
verbose = app.Flag("verbose", "Run in verbose mode.").
|
||||
Short('v').
|
||||
Envar("MTG_VERBOSE").
|
||||
Bool()
|
||||
|
||||
bindIP = app.Flag("bind-ip", "Which IP to bind to.").
|
||||
Short('b').
|
||||
Envar("MTG_IP").
|
||||
Default("127.0.0.1").
|
||||
IP()
|
||||
bindPort = app.Flag("bind-port", "Which port to bind to.").
|
||||
Short('p').
|
||||
Envar("MTG_PORT").
|
||||
Default("3128").
|
||||
Uint16()
|
||||
|
||||
publicIPv4 = app.Flag("public-ipv4", "Which IPv4 address is public.").
|
||||
Short('4').
|
||||
Envar("MTG_IPV4").
|
||||
IP()
|
||||
publicIPv4Port = app.Flag("public-ipv4-port", "Which IPv4 port is public. Default is 'bind-port' value.").
|
||||
Envar("MTG_IPV4_PORT").
|
||||
Uint16()
|
||||
|
||||
publicIPv6 = app.Flag("public-ipv6", "Which IPv6 address is public.").
|
||||
Short('6').
|
||||
Envar("MTG_IPV6").
|
||||
IP()
|
||||
publicIPv6Port = app.Flag("public-ipv6-port", "Which IPv6 port is public. Default is 'bind-port' value.").
|
||||
Envar("MTG_IPV6_PORT").
|
||||
Uint16()
|
||||
|
||||
statsIP = app.Flag("stats-ip", "Which IP bind stats server to.").
|
||||
Short('t').
|
||||
Envar("MTG_STATS_IP").
|
||||
Default("127.0.0.1").
|
||||
IP()
|
||||
statsPort = app.Flag("stats-port", "Which port bind stats to.").
|
||||
Short('q').
|
||||
Envar("MTG_STATS_PORT").
|
||||
Default("3129").
|
||||
Uint16()
|
||||
|
||||
statsdIP = app.Flag("statsd-ip", "Which IP should we use for working with statsd.").
|
||||
Envar("MTG_STATSD_IP").
|
||||
String()
|
||||
statsdPort = app.Flag("statsd-port", "Which port should we use for working with statsd.").
|
||||
Envar("MTG_STATSD_PORT").
|
||||
Default("8125").
|
||||
Uint16()
|
||||
statsdNetwork = app.Flag("statsd-network", "Which network is used to work with statsd. Only 'tcp' and 'udp' are supported.").
|
||||
Envar("MTG_STATSD_NETWORK").
|
||||
Default("udp").
|
||||
String()
|
||||
statsdPrefix = app.Flag("statsd-prefix", "Which bucket prefix should we use for sending stats to statsd.").
|
||||
Envar("MTG_STATSD_PREFIX").
|
||||
Default("mtg").
|
||||
String()
|
||||
statsdTagsFormat = app.Flag("statsd-tags-format", "Which tag format should we use to send stats metrics. Valid options are 'datadog' and 'influxdb'.").
|
||||
Envar("MTG_STATSD_TAGS_FORMAT").
|
||||
String()
|
||||
statsdTags = app.Flag("statsd-tags", "Tags to use for working with statsd (specified as 'key=value').").
|
||||
Envar("MTG_STATSD_TAGS").
|
||||
StringMap()
|
||||
|
||||
secret = app.Arg("secret", "Secret of this proxy.").Required().String()
|
||||
adtag = app.Arg("adtag", "ADTag of the proxy.").String()
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
app.Version(version)
|
||||
|
||||
}
|
||||
var version = "dev" // has to be set by ldflags
|
||||
|
||||
func main() {
|
||||
kingpin.MustParse(app.Parse(os.Args[1:]))
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
err := setRLimit()
|
||||
if err != nil {
|
||||
usage(err.Error())
|
||||
}
|
||||
|
||||
conf, err := config.NewConfig(*debug, *verbose,
|
||||
*bindIP, *bindPort,
|
||||
*publicIPv4, *publicIPv4Port,
|
||||
*publicIPv6, *publicIPv6Port,
|
||||
*statsIP, *statsPort,
|
||||
*secret, *adtag,
|
||||
*statsdIP, *statsdPort, *statsdNetwork, *statsdPrefix,
|
||||
*statsdTagsFormat, *statsdTags,
|
||||
)
|
||||
if err != nil {
|
||||
usage(err.Error())
|
||||
}
|
||||
|
||||
atom := zap.NewAtomicLevel()
|
||||
if conf.Debug {
|
||||
atom.SetLevel(zapcore.DebugLevel)
|
||||
} else if conf.Verbose {
|
||||
atom.SetLevel(zapcore.InfoLevel)
|
||||
} else {
|
||||
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
|
||||
|
||||
printURLs(conf.GetURLs())
|
||||
|
||||
if conf.UseMiddleProxy() {
|
||||
zap.S().Infow("Use middle proxy connection to Telegram")
|
||||
if diff, err := ntp.Fetch(); err != nil {
|
||||
zap.S().Warnw("Could not fetch time data from NTP")
|
||||
} else {
|
||||
if diff >= time.Second {
|
||||
usage(fmt.Sprintf("You choose to use middle proxy but your clock drift (%s) is bigger than 1 second. Please, sync your time", diff))
|
||||
}
|
||||
go ntp.AutoUpdate()
|
||||
}
|
||||
} else {
|
||||
zap.S().Infow("Use direct connection to Telegram")
|
||||
}
|
||||
|
||||
if err := stats.Start(conf); err != nil {
|
||||
if err := utils.SetLimits(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server := proxy.NewProxy(conf)
|
||||
if err := server.Serve(); err != nil {
|
||||
zap.S().Fatalw("Server stopped", "error", err)
|
||||
}
|
||||
}
|
||||
cli := &cli.CLI{}
|
||||
ctx := kong.Parse(cli, kong.Vars{
|
||||
"version": version,
|
||||
})
|
||||
|
||||
func setRLimit() (err error) {
|
||||
rLimit := syscall.Rlimit{}
|
||||
err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
err = errors.Annotate(err, "Cannot get rlimit")
|
||||
return
|
||||
}
|
||||
rLimit.Cur = rLimit.Max
|
||||
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
err = errors.Annotate(err, "Cannot set rlimit")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func printURLs(data interface{}) {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func usage(msg string) {
|
||||
io.WriteString(os.Stderr, msg+"\n") // nolint: errcheck
|
||||
os.Exit(1)
|
||||
ctx.FatalIfErrorf(ctx.Run(cli, version))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package mtglib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type connTraffic struct {
|
||||
net.Conn
|
||||
|
||||
streamID string
|
||||
stream EventStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (c connTraffic) Read(b []byte) (int, error) {
|
||||
n, err := c.Conn.Read(b)
|
||||
|
||||
if n > 0 {
|
||||
c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), true))
|
||||
}
|
||||
|
||||
return n, err // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (c connTraffic) Write(b []byte) (int, error) {
|
||||
n, err := c.Conn.Write(b)
|
||||
|
||||
if n > 0 {
|
||||
c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), false))
|
||||
}
|
||||
|
||||
return n, err // nolint: wrapcheck
|
||||
}
|
||||
|
||||
type connRewind struct {
|
||||
net.Conn
|
||||
|
||||
active io.Reader
|
||||
buf bytes.Buffer
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *connRewind) Read(p []byte) (int, error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
return c.active.Read(p) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (c *connRewind) Rewind() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.active = io.MultiReader(&c.buf, c.Conn)
|
||||
}
|
||||
|
||||
func newConnRewind(conn net.Conn) *connRewind {
|
||||
rv := &connRewind{
|
||||
Conn: conn,
|
||||
}
|
||||
rv.active = io.TeeReader(conn, &rv.buf)
|
||||
|
||||
return rv
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package mtglib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/testlib"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ConnRewindBaseConn struct {
|
||||
testlib.NetConnMock
|
||||
|
||||
readBuffer bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *ConnRewindBaseConn) Read(p []byte) (int, error) {
|
||||
c.Called(p)
|
||||
|
||||
return c.readBuffer.Read(p) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
type ConnTrafficTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
eventStreamMock *EventStreamMock
|
||||
connMock *testlib.NetConnMock
|
||||
conn io.ReadWriter
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) SetupTest() {
|
||||
suite.eventStreamMock = &EventStreamMock{}
|
||||
suite.connMock = &testlib.NetConnMock{}
|
||||
suite.conn = connTraffic{
|
||||
Conn: suite.connMock,
|
||||
streamID: "CONNID",
|
||||
ctx: context.Background(),
|
||||
stream: suite.eventStreamMock,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TearDownTest() {
|
||||
suite.eventStreamMock.AssertExpectations(suite.T())
|
||||
suite.connMock.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestReadOk() {
|
||||
suite.eventStreamMock.
|
||||
On("Send", mock.Anything, mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
evt, ok := args.Get(1).(EventTraffic)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal("CONNID", evt.StreamID())
|
||||
suite.WithinDuration(time.Now(), evt.Timestamp(), time.Second)
|
||||
suite.EqualValues(10, evt.Traffic)
|
||||
suite.True(evt.IsRead)
|
||||
})
|
||||
suite.connMock.On("Read", mock.Anything).Once().Return(10, nil)
|
||||
|
||||
n, err := suite.conn.Read(make([]byte, 10))
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestReadErr() { // nolint: dupl
|
||||
suite.eventStreamMock.
|
||||
On("Send", mock.Anything, mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
evt, ok := args.Get(1).(EventTraffic)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal("CONNID", evt.StreamID())
|
||||
suite.WithinDuration(time.Now(), evt.Timestamp(), time.Second)
|
||||
suite.EqualValues(10, evt.Traffic)
|
||||
suite.True(evt.IsRead)
|
||||
})
|
||||
suite.connMock.On("Read", mock.Anything).Once().Return(10, io.EOF)
|
||||
|
||||
n, err := suite.conn.Read(make([]byte, 10))
|
||||
suite.True(errors.Is(err, io.EOF))
|
||||
suite.Equal(10, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestReadNothingOk() {
|
||||
suite.connMock.On("Read", mock.Anything).Once().Return(0, nil)
|
||||
|
||||
n, err := suite.conn.Read(make([]byte, 10))
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestReadNothingErr() {
|
||||
suite.connMock.On("Read", mock.Anything).Once().Return(0, io.EOF)
|
||||
|
||||
n, err := suite.conn.Read(make([]byte, 10))
|
||||
suite.True(errors.Is(err, io.EOF))
|
||||
suite.Equal(0, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestWriteOk() {
|
||||
suite.eventStreamMock.
|
||||
On("Send", mock.Anything, mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
evt, ok := args.Get(1).(EventTraffic)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal("CONNID", evt.StreamID())
|
||||
suite.WithinDuration(time.Now(), evt.Timestamp(), time.Second)
|
||||
suite.EqualValues(10, evt.Traffic)
|
||||
suite.False(evt.IsRead)
|
||||
})
|
||||
suite.connMock.On("Write", mock.Anything).Once().Return(10, nil)
|
||||
|
||||
n, err := suite.conn.Write(make([]byte, 10))
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestWriteErr() { // nolint: dupl
|
||||
suite.eventStreamMock.
|
||||
On("Send", mock.Anything, mock.Anything).
|
||||
Once().
|
||||
Run(func(args mock.Arguments) {
|
||||
evt, ok := args.Get(1).(EventTraffic)
|
||||
|
||||
suite.True(ok)
|
||||
suite.Equal("CONNID", evt.StreamID())
|
||||
suite.WithinDuration(time.Now(), evt.Timestamp(), time.Second)
|
||||
suite.EqualValues(10, evt.Traffic)
|
||||
suite.False(evt.IsRead)
|
||||
})
|
||||
suite.connMock.On("Write", mock.Anything).Once().Return(10, io.EOF)
|
||||
|
||||
n, err := suite.conn.Write(make([]byte, 10))
|
||||
suite.True(errors.Is(err, io.EOF))
|
||||
suite.Equal(10, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestWriteNothingOk() {
|
||||
suite.connMock.On("Write", mock.Anything).Once().Return(0, nil)
|
||||
|
||||
n, err := suite.conn.Write(make([]byte, 10))
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, n)
|
||||
}
|
||||
|
||||
func (suite *ConnTrafficTestSuite) TestWriteNothingErr() {
|
||||
suite.connMock.On("Write", mock.Anything).Once().Return(0, io.EOF)
|
||||
|
||||
n, err := suite.conn.Write(make([]byte, 10))
|
||||
suite.True(errors.Is(err, io.EOF))
|
||||
suite.Equal(0, n)
|
||||
}
|
||||
|
||||
type ConnRewindTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
connMock *ConnRewindBaseConn
|
||||
conn *connRewind
|
||||
}
|
||||
|
||||
func (suite *ConnRewindTestSuite) SetupTest() {
|
||||
suite.connMock = &ConnRewindBaseConn{}
|
||||
suite.conn = newConnRewind(suite.connMock)
|
||||
}
|
||||
|
||||
func (suite *ConnRewindTestSuite) TearDownTest() {
|
||||
suite.connMock.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func (suite *ConnRewindTestSuite) TestRead() {
|
||||
suite.connMock.On("Read", mock.Anything)
|
||||
suite.connMock.readBuffer.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
|
||||
|
||||
buf := make([]byte, 2)
|
||||
|
||||
n, err := suite.conn.Read(buf)
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, n)
|
||||
suite.Equal([]byte{1, 2}, buf)
|
||||
|
||||
n, err = suite.conn.Read(buf)
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, n)
|
||||
suite.Equal([]byte{3, 4}, buf)
|
||||
|
||||
suite.conn.Rewind()
|
||||
|
||||
data, err := io.ReadAll(suite.conn)
|
||||
suite.NoError(err)
|
||||
suite.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, data)
|
||||
}
|
||||
|
||||
func TestConnTraffic(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ConnTrafficTestSuite{})
|
||||
}
|
||||
|
||||
func TestConnRewind(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ConnRewindTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package mtglib
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type eventBase struct {
|
||||
streamID string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// StreamID returns a ID of the stream this event belongs to.
|
||||
func (e eventBase) StreamID() string {
|
||||
return e.streamID
|
||||
}
|
||||
|
||||
// Timestamp return a time when this event was generated.
|
||||
func (e eventBase) Timestamp() time.Time {
|
||||
return e.timestamp
|
||||
}
|
||||
|
||||
// EventStart is emitted when mtg proxy starts to process a new
|
||||
// connection.
|
||||
type EventStart struct {
|
||||
eventBase
|
||||
|
||||
// RemoteIP is an IP address of the client.
|
||||
RemoteIP net.IP
|
||||
}
|
||||
|
||||
// EventConnectedToDC is emitted when mtg proxy has connected to a
|
||||
// Telegram server.
|
||||
type EventConnectedToDC struct {
|
||||
eventBase
|
||||
|
||||
// RemoteIP is an IP address of the Telegram server proxy has been
|
||||
// connected to.
|
||||
RemoteIP net.IP
|
||||
|
||||
// DC is an index of the datacenter proxy has been connected to.
|
||||
DC int
|
||||
}
|
||||
|
||||
// EventTraffic is emitted when we read/write some bytes on a connection.
|
||||
type EventTraffic struct {
|
||||
eventBase
|
||||
|
||||
// Traffic is a count of bytes which were transmitted.
|
||||
Traffic uint
|
||||
|
||||
// IsRead defines if we _read_ or _write_ to connection. A rule of
|
||||
// thumb is simple: EventTraffic is bound to a remote connection. Not
|
||||
// to a client one, but either to Telegram or front domain one.
|
||||
//
|
||||
// In the case of Telegram, isRead means that we've fetched some bytes
|
||||
// from Telegram to send it to a client.
|
||||
//
|
||||
// In the case of the front domain, it means that we've fetched some
|
||||
// bytes from this domain to send it to a client.
|
||||
IsRead bool
|
||||
}
|
||||
|
||||
// EventFinish is emitted when we stop to manage a connection.
|
||||
type EventFinish struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
// EventDomainFronting is emitted when we connect to a front domain
|
||||
// instead of Telegram server.
|
||||
type EventDomainFronting struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
// EventConcurrencyLimited is emitted when connection was declined
|
||||
// because of the concurrency limit of the worker pool.
|
||||
type EventConcurrencyLimited struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
// EventIPBlocklisted is emitted when connection was declined because
|
||||
// IP address was found in IP blocklist.
|
||||
type EventIPBlocklisted struct {
|
||||
eventBase
|
||||
|
||||
RemoteIP net.IP
|
||||
}
|
||||
|
||||
// EventReplayAttack is emitted when mtg detects a replay attack on a
|
||||
// connection.
|
||||
type EventReplayAttack struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
// NewEventStart creates a new EventStart event.
|
||||
func NewEventStart(streamID string, remoteIP net.IP) EventStart {
|
||||
return EventStart{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
streamID: streamID,
|
||||
},
|
||||
RemoteIP: remoteIP,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventConnectedToDC creates a new EventConnectedToDC event.
|
||||
func NewEventConnectedToDC(streamID string, remoteIP net.IP, dc int) EventConnectedToDC {
|
||||
return EventConnectedToDC{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
streamID: streamID,
|
||||
},
|
||||
RemoteIP: remoteIP,
|
||||
DC: dc,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventTraffic creates a new EventTraffic event.
|
||||
func NewEventTraffic(streamID string, traffic uint, isRead bool) EventTraffic {
|
||||
return EventTraffic{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
streamID: streamID,
|
||||
},
|
||||
Traffic: traffic,
|
||||
IsRead: isRead,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventFinish creates a new EventFinish event.
|
||||
func NewEventFinish(streamID string) EventFinish {
|
||||
return EventFinish{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
streamID: streamID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventDomainFronting creates a new EventDomainFronting event.
|
||||
func NewEventDomainFronting(streamID string) EventDomainFronting {
|
||||
return EventDomainFronting{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
streamID: streamID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventConcurrencyLimited creates a new EventConcurrencyLimited
|
||||
// event.
|
||||
func NewEventConcurrencyLimited() EventConcurrencyLimited {
|
||||
return EventConcurrencyLimited{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventIPBlocklisted creates a new EventIPBlocklisted event.
|
||||
func NewEventIPBlocklisted(remoteIP net.IP) EventIPBlocklisted {
|
||||
return EventIPBlocklisted{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
},
|
||||
RemoteIP: remoteIP,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEventReplayAttack creates a new EventReplayAttack event.
|
||||
func NewEventReplayAttack(streamID string) EventReplayAttack {
|
||||
return EventReplayAttack{
|
||||
eventBase: eventBase{
|
||||
timestamp: time.Now(),
|
||||
streamID: streamID,
|
||||
},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user