mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 18:04:03 +03:00
REPOSITORY / ScuroNeko/mtg
Compare commits
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79f54a4e67 | ||
|
|
4b78e83be7 | ||
|
|
ad30eca406 | ||
|
|
ecf947b86c | ||
|
|
5be581154e | ||
|
|
bc3b517e8b | ||
|
|
3f8f96b91f | ||
|
|
ef55fbba15 | ||
|
|
e7416bc04d | ||
|
|
9a6264a89f | ||
|
|
33e0509c5a | ||
|
|
ffad717829 | ||
|
|
7b1f86b75d | ||
|
|
a5e59d9ef7 | ||
|
|
16c06f247c | ||
|
|
0ddaabb136 | ||
|
|
558fec60de | ||
|
|
cc101c9a47 | ||
|
|
e6fa69d288 | ||
|
|
c14a2329c5 | ||
|
|
4c75066ef8 | ||
|
|
ce8163d1b7 | ||
|
|
ca77157fd5 | ||
|
|
66f4d967e7 | ||
|
|
d19cfb1df4 | ||
|
|
3540408adf | ||
|
|
7917434a37 | ||
|
|
853395106b | ||
|
|
d1b0d1f133 | ||
|
|
73bd7287f0 | ||
|
|
31b8ab4482 | ||
|
|
bae5407372 | ||
|
|
4814b0fcc1 | ||
|
|
3ce549bb16 | ||
|
|
5c636a68dc | ||
|
|
9f6f906786 | ||
|
|
eb32766c1f | ||
|
|
787d72cf52 | ||
|
|
ce8f1ebb6c | ||
|
|
cd29f3e20b | ||
|
|
3e105f2beb | ||
|
|
6bbdd99e7f | ||
|
|
894c68019e | ||
|
|
98f18fc22b | ||
|
|
01b739aa7c | ||
|
|
dbaa743e03 | ||
|
|
d1e5f9d145 | ||
|
|
686f177ab9 | ||
|
|
706aef8ca1 | ||
|
|
14dfb9506a | ||
|
|
f742066c54 | ||
|
|
fbe4d32590 | ||
|
|
d0f18be91d | ||
|
|
929b73e2eb | ||
|
|
6b7364238a | ||
|
|
866906e770 | ||
|
|
b3c8c4a47d | ||
|
|
fbc7499cda | ||
|
|
ca5800cf60 | ||
|
|
00bb7be900 | ||
|
|
e8c70603f7 | ||
|
|
9d72904508 | ||
|
|
456ed5b051 | ||
|
|
4b7be8c565 | ||
|
|
8758208de6 | ||
|
|
6cd91600b5 | ||
|
|
bd39fa63cb | ||
|
|
eaa757b6d0 | ||
|
|
dc99dd165e | ||
|
|
7c70fd8079 | ||
|
|
dcbbb49607 | ||
|
|
1050ca0b97 | ||
|
|
c85c88efd6 | ||
|
|
c53364d952 | ||
|
|
ed91290e47 | ||
|
|
3fd5e9eb19 | ||
|
|
87ed1d1aa7 | ||
|
|
ec4f0656fb | ||
|
|
0c5d487fae | ||
|
|
889ab6c227 | ||
|
|
e1864377c2 | ||
|
|
4c8925ab65 | ||
|
|
bf34f0c92d | ||
|
|
604b1d2597 | ||
|
|
b19f491fc8 | ||
|
|
16f9ec690b | ||
|
|
fec60083a3 | ||
|
|
0882d45d6a | ||
|
|
86c6541e0d | ||
|
|
cc1f60c4a6 | ||
|
|
700f71bb31 | ||
|
|
097b52c8ef | ||
|
|
c3ffd7a307 | ||
|
|
fe3b0f3ca5 | ||
|
|
2273a0e4af | ||
|
|
115510985a | ||
|
|
2b5b325722 | ||
|
|
c536cbecd5 | ||
|
|
5e9d0649a2 | ||
|
|
585ebfeb50 | ||
|
|
81bca75c25 | ||
|
|
4f048a3b82 | ||
|
|
43f5d2131e | ||
|
|
e2073f0585 | ||
|
|
0652f322fa | ||
|
|
d3aa0f2de5 | ||
|
|
40c6cf294a | ||
|
|
4c38ea2b11 | ||
|
|
e6d444546f | ||
|
|
611583ba88 | ||
|
|
463af648ce | ||
|
|
b748603096 | ||
|
|
54a7c6a2a5 | ||
|
|
a3bae795c4 | ||
|
|
372c703ad6 | ||
|
|
0dd890a09d | ||
|
|
0274b3436a | ||
|
|
04b88cc864 | ||
|
|
b3112c5c67 | ||
|
|
3661fe108d | ||
|
|
5d5b77d73b | ||
|
|
7bffdcdf4a | ||
|
|
ea7033d569 | ||
|
|
24fdec6694 | ||
|
|
2fe04c7c9b | ||
|
|
7b5cb8e14b | ||
|
|
8566cede74 | ||
|
|
2dc30c5837 | ||
|
|
2cdff017e2 | ||
|
|
766800700a | ||
|
|
7e7823118e | ||
|
|
784a5818c8 | ||
|
|
75b95995a9 | ||
|
|
5941f0674a | ||
|
|
155a97c25d | ||
|
|
1fd0ebea9b | ||
|
|
988f61718c | ||
|
|
3d9a63a1f0 | ||
|
|
6c92ec314d | ||
|
|
132a01c27e | ||
|
|
b91f28d7d9 | ||
|
|
339840c14a | ||
|
|
a368975959 | ||
|
|
102e7610be | ||
|
|
e78c4d3bde | ||
|
|
1925efc2ec | ||
|
|
9562a86038 | ||
|
|
21d4f3d101 | ||
|
|
38ff62fddc | ||
|
|
26070d5b3e | ||
|
|
03d98ee256 | ||
|
|
dc28677086 | ||
|
|
54f4b397b6 | ||
|
|
96bcb6cd8e | ||
|
|
491b674e70 | ||
|
|
fdbee86d5e | ||
|
|
5b0d54e74d | ||
|
|
64754d1039 | ||
|
|
23c42def4f | ||
|
|
87a720dd58 | ||
|
|
10b78322a3 | ||
|
|
841a4d2227 | ||
|
|
3cacd74e12 | ||
|
|
71d4c6c42f | ||
|
|
75b05b9b14 | ||
|
|
daa8b9c798 | ||
|
|
bef14bd009 | ||
|
|
36d695118e | ||
|
|
4c3f42e264 | ||
|
|
3992054560 | ||
|
|
db8614999a | ||
|
|
bc2bd4510a | ||
|
|
5eca6ecb05 | ||
|
|
ce30e128e5 | ||
|
|
dedd67780b | ||
|
|
d8d954bbe8 | ||
|
|
bddf180575 | ||
|
|
9b78f766e8 | ||
|
|
e2e464d32c | ||
|
|
40b483357f | ||
|
|
f0efa4697e | ||
|
|
6d92d5fe21 | ||
|
|
c3e8e8b1fe | ||
|
|
a444a10bd0 | ||
|
|
d3551aa9cc | ||
|
|
4a2d1df384 | ||
|
|
a3c64c1d1e | ||
|
|
bbe8f34e72 | ||
|
|
02aa969d6c | ||
|
|
4da2a4f10f | ||
|
|
336e825e3a | ||
|
|
b42e51e300 | ||
|
|
6b1bfe7b17 | ||
|
|
6219f4bd90 | ||
|
|
3076c52adb | ||
|
|
933855a233 | ||
|
|
ea698259d5 | ||
|
|
aefa003355 | ||
|
|
e3b7472239 | ||
|
|
a3362c7ea4 | ||
|
|
66c45dc83b | ||
|
|
f7c33ee333 | ||
|
|
42160a08fe | ||
|
|
925a02dac3 | ||
|
|
7108fe99ec | ||
|
|
69203f3e23 | ||
|
|
4d2d21e101 | ||
|
|
8de727932d | ||
|
|
188fa6a04a | ||
|
|
0330a0e5cd | ||
|
|
0ad2d61742 | ||
|
|
f155b9f37e | ||
|
|
3a44bcb854 | ||
|
|
58335b3e59 | ||
|
|
657a74a5c2 | ||
|
|
f3112d4ba6 | ||
|
|
172b596cf1 | ||
|
|
b258581f47 | ||
|
|
60d5b3cadd | ||
|
|
f21ee40baf | ||
|
|
adf4ab1a35 | ||
|
|
83eeedc008 | ||
|
|
57cb1b5aa0 | ||
|
|
5e31b95bb5 | ||
|
|
80a1de60e6 | ||
|
|
2408f1530f | ||
|
|
23519913f2 | ||
|
|
7955ac6a46 | ||
|
|
dbbfdaf918 | ||
|
|
8e7207d975 | ||
|
|
c04f9e392a | ||
|
|
46bd617581 | ||
|
|
7617aeadda | ||
|
|
e009d05a90 | ||
|
|
f0063ba089 | ||
|
|
b08d945d7c | ||
|
|
abff0cf211 | ||
|
|
f52b3391d1 | ||
|
|
cae33a22e6 | ||
|
|
3dc263d6d7 | ||
|
|
8eb5fed92f | ||
|
|
ab1b3b4863 | ||
|
|
ab585fd699 | ||
|
|
91a1ee956c | ||
|
|
f8ad90c845 | ||
|
|
37a78bd1c3 | ||
|
|
18e3ca77de | ||
|
|
185baf6bc9 | ||
|
|
546a5849f3 | ||
|
|
2cc81219b1 | ||
|
|
1f862027af | ||
|
|
02643fb6bb | ||
|
|
e167b85d3b | ||
|
|
c0b6124d94 | ||
|
|
113b5ecbe0 | ||
|
|
4daf927a72 | ||
|
|
1218f18af9 | ||
|
|
7ca143d352 | ||
|
|
428010880e | ||
|
|
dc81740bda | ||
|
|
f7dc54b652 | ||
|
|
c0ab254acf | ||
|
|
5c4536c591 | ||
|
|
d48e82be7c | ||
|
|
bc4b14c83d | ||
|
|
6b9b437a5a | ||
|
|
d6566e5dfb | ||
|
|
6b28488fbd | ||
|
|
1a02511afe | ||
|
|
757ea5b63c | ||
|
|
24add0dce4 | ||
|
|
299c6478c2 | ||
|
|
ddd3d608fa | ||
|
|
d5b0a47d52 | ||
|
|
ca0a550f12 | ||
|
|
94084674c0 | ||
|
|
9aa56191f9 | ||
|
|
a34949f15c | ||
|
|
cbcc113e41 | ||
|
|
6f0b0e0c32 | ||
|
|
098a9c411a | ||
|
|
d1dd56550f | ||
|
|
59f328b804 | ||
|
|
783c49db37 | ||
|
|
015f02c077 | ||
|
|
e0833e86d9 | ||
|
|
f89f5f8469 | ||
|
|
0395ce8d1a | ||
|
|
ef87d031ed | ||
|
|
ba875da9b5 | ||
|
|
8f7e79862b | ||
|
|
9124ffabf1 | ||
|
|
c8914f90db | ||
|
|
7c43a4b0b7 | ||
|
|
e386ae0daf | ||
|
|
b5346668f8 | ||
|
|
7002e4cd09 | ||
|
|
ecfd550a95 | ||
|
|
5b46152945 | ||
|
|
88e076a283 | ||
|
|
0ed7e10379 | ||
|
|
21ee1e2c6c | ||
|
|
09f8233bc6 | ||
|
|
d5147f1935 | ||
|
|
4689479745 | ||
|
|
2be900745f | ||
|
|
baee322cd7 | ||
|
|
28fe63dbdb | ||
|
|
4aab3b686c | ||
|
|
121200cfad | ||
|
|
a8c4d0a9c4 | ||
|
|
790bf21166 | ||
|
|
e5b9c841cd | ||
|
|
8ccf61c23e | ||
|
|
0e033b59b3 | ||
|
|
cdd93bd25f | ||
|
|
cac476901d | ||
|
|
bb21bf814f | ||
|
|
68e1d66017 |
@@ -0,0 +1,4 @@
|
||||
---
|
||||
|
||||
fixes:
|
||||
- "github.com/9seconds/mtg/v2/::"
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
name: CI
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -30,8 +42,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
go_version:
|
||||
- ~1.14
|
||||
- ~1.15
|
||||
- ~1.16
|
||||
- ^1.17
|
||||
steps:
|
||||
@@ -73,12 +83,12 @@ jobs:
|
||||
- name: Run linter
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.42.0
|
||||
version: v1.43.0
|
||||
|
||||
docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions
|
||||
# https://github.com/github/codeql-action/issues/572
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
security-events: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
@@ -9,3 +9,5 @@ mtg
|
||||
vendor/
|
||||
ccbuilds/
|
||||
.bin/
|
||||
coverage.txt
|
||||
dist/
|
||||
|
||||
+1
-2
@@ -3,11 +3,10 @@ concurrency = 4
|
||||
deadline = "2m"
|
||||
tests = true
|
||||
skip-dirs = ["vendor"]
|
||||
skip-files = ["version.go"]
|
||||
|
||||
[output]
|
||||
format = "colored-line-number"
|
||||
|
||||
[linters]
|
||||
enable-all = true
|
||||
disable = ["gochecknoglobals", "gas", "gomnd", "goerr113", "exhaustivestruct"]
|
||||
disable = ["ireturn", "varnamelen", "gochecknoglobals", "gas", "goerr113", "exhaustivestruct"]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
|
||||
project_name: mtg
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- binary: '{{ .ProjectName }}'
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
- -mod=readonly
|
||||
ldflags: -s -w -X main.version={{ .Version }}
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
- goos: netbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
|
||||
archives:
|
||||
- name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||
format: tar.gz
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- SECURITY.md
|
||||
|
||||
gomod:
|
||||
proxy: true
|
||||
|
||||
snapshot:
|
||||
name_template: '{{ .Version }}'
|
||||
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
name_template: '{{ .ProjectName }}-sources'
|
||||
+2
-3
@@ -24,9 +24,8 @@ RUN set -x \
|
||||
FROM scratch
|
||||
|
||||
ENTRYPOINT ["/mtg"]
|
||||
ENV MTG_BIND=0.0.0.0:3128 \
|
||||
MTG_STATS_BIND=0.0.0.0:3129
|
||||
EXPOSE 3128 3129
|
||||
CMD ["run", "/config.toml"]
|
||||
|
||||
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
|
||||
|
||||
@@ -2,14 +2,12 @@ ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
IMAGE_NAME := mtg
|
||||
APP_NAME := $(IMAGE_NAME)
|
||||
|
||||
CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}")
|
||||
|
||||
GOLANGCI_LINT_VERSION := v1.42.0
|
||||
GOLANGCI_LINT_VERSION := v1.43.0
|
||||
|
||||
VERSION_GO := $(shell go version)
|
||||
VERSION_DATE := $(shell date -Ru)
|
||||
VERSION_TAG := $(shell git describe --tags --always)
|
||||
COMMON_BUILD_FLAGS := -mod=readonly -ldflags="-s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
|
||||
COMMON_BUILD_FLAGS := -trimpath -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
|
||||
|
||||
GOBIN := $(ROOT_DIR)/.bin
|
||||
GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
|
||||
@@ -27,33 +25,29 @@ $(APP_NAME): build
|
||||
|
||||
.PHONY: static
|
||||
static:
|
||||
@env CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
||||
|
||||
$(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
|
||||
$(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
|
||||
$(APP_NAME)-%: ccbuilds
|
||||
@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
|
||||
go build \
|
||||
@env CGO_ENABLED=0 GOOS=linux go build \
|
||||
$(COMMON_BUILD_FLAGS) \
|
||||
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
|
||||
|
||||
.PHONY: ccbuilds
|
||||
ccbuilds:
|
||||
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
|
||||
-tags netgo \
|
||||
-a \
|
||||
-o "$(APP_NAME)"
|
||||
|
||||
vendor: go.mod go.sum
|
||||
@$(MOD_ON) go mod vendor
|
||||
|
||||
.bin:
|
||||
@mkdir -p "$(GOBIN)" || true
|
||||
|
||||
.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 -race -v ./...
|
||||
|
||||
.PHONY: crosscompile
|
||||
crosscompile: $(CC_BINARIES)
|
||||
@go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./...
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@@ -65,6 +59,12 @@ clean:
|
||||
lint:
|
||||
@$(GOTOOL) golangci-lint run
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
@$(GOTOOL) goreleaser release --snapshot --rm-dist && \
|
||||
find "$(ROOT_DIR)/dist" -type d | grep -vP "dist$$" | xargs -r rm -rf && \
|
||||
rm -f "$(ROOT_DIR)/dist/config.yaml"
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
|
||||
@@ -74,19 +74,25 @@ doc:
|
||||
@$(GOTOOL) godoc -http 0.0.0.0:10000
|
||||
|
||||
.PHONY: install-tools
|
||||
install-tools: install-tools-lint install-tools-godoc
|
||||
install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt install-tools-goreleaser
|
||||
|
||||
.PHONY: install-tools-lint
|
||||
install-tools-lint:
|
||||
@mkdir -p "$(GOBIN)" || true && \
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
|
||||
install-tools-lint: .bin
|
||||
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
|
||||
| bash -s -- -b "$(GOBIN)" "$(GOLANGCI_LINT_VERSION)"
|
||||
|
||||
.PHONY: install-tools-godoc
|
||||
install-tools-godoc:
|
||||
@mkdir -p "$(GOBIN)" || true && \
|
||||
$(GOTOOL) go get -u golang.org/x/tools/cmd/godoc
|
||||
install-tools-godoc: .bin
|
||||
@$(GOTOOL) go install golang.org/x/tools/cmd/godoc@latest
|
||||
|
||||
.PHONY: install-tools-gofumpt
|
||||
install-tools-gofumpt: .bin
|
||||
@$(GOTOOL) go install mvdan.cc/gofumpt@latest
|
||||
|
||||
.PHONY: goreleaser
|
||||
install-tools-goreleaser: .bin
|
||||
@$(GOTOOL) go install github.com/goreleaser/goreleaser@latest
|
||||
|
||||
.PHONY: update-deps
|
||||
upgrade-deps:
|
||||
$go get -u && go mod tidy
|
||||
update-deps:
|
||||
@go get -u && go mod tidy
|
||||
|
||||
@@ -1,14 +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://goreportcard.com/report/github.com/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)
|
||||
|
||||
**Please see a guide on upgrading to 1.0 at the end of this README.**
|
||||
**If you use v1.0 or upgrade broke you proxy, please read the chapter
|
||||
[Version 2](#version-2)**
|
||||
|
||||
# Rationale
|
||||
## Rationale
|
||||
|
||||
There are several available proxies for Telegram MTPROTO available. Here
|
||||
are the most notable:
|
||||
@@ -17,267 +19,366 @@ are the most notable:
|
||||
* [Python](https://github.com/alexbers/mtprotoproxy)
|
||||
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
||||
|
||||
Almost all of them follow the way how official proxy was built. 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 few 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.
|
||||
[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 complexify
|
||||
|
||||
I think that multiple secrets solve no problems and just complex
|
||||
software. I also believe that in the case of throwout proxies, this
|
||||
feature is a useless luxury.
|
||||
* **Minimum docker image size**
|
||||
Official image is less than 3.5 megabytes. Literally.
|
||||
the feature is a useless luxury.
|
||||
|
||||
* **No adtag support**
|
||||
|
||||
Please read [Version 2](#version-2) chapter.
|
||||
|
||||
* **No management WebUI**
|
||||
|
||||
This is an implementation of a simple lightweight proxy. I won't do that.
|
||||
|
||||
This proxy supports 2 modes of work: direct connection to Telegram and
|
||||
promoted channel mode. If you do not need promoted channels, I would
|
||||
recommend you to go with direct mode: this way is more robust.
|
||||
* **Proxy chaining**
|
||||
|
||||
To run a 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.
|
||||
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.
|
||||
|
||||
To get promoted channel, please contact
|
||||
[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
|
||||
second parameter.
|
||||
* **Native blocklist support**
|
||||
|
||||
Previously, this was delegated to the [FireHOL](https://firehol.org/)
|
||||
project or similar ones which track attacks and publish a list of
|
||||
potentially dangerous IPs. mtg has native support of such blocklists.
|
||||
|
||||
# Source code organization
|
||||
* **Can be used as a library**
|
||||
|
||||
There are 2 main branches:
|
||||
mtg v2 was redesigned in a way so it can be embedded into your
|
||||
software (written in Golang) with a minimum effort + you can replace
|
||||
some parts with those you want.
|
||||
|
||||
1. `master` branch contains potentially unstable features
|
||||
2. `stable` branch contains stable version. Usually you want to use this branch.
|
||||
### Version 2
|
||||
|
||||
# How to build
|
||||
If you use version 1.x before, you are probably noticed some major
|
||||
backward non-compatible details:
|
||||
|
||||
1. Configuration file
|
||||
2. Removed support of adtag
|
||||
|
||||
For the configuration file, please check out the full example in this
|
||||
repository. It has a lot of comments and most of the options are
|
||||
optional. We do have only `secret` and `bind-to` sections mandatory.
|
||||
Other sections in the example configuration file are filled with default
|
||||
values.
|
||||
|
||||
Adtag support was removed completely. This was done to debloat mtg and
|
||||
keep it simple and obvious. Hopefully, this goal is achieved and the
|
||||
source code is clean and straightforward enough.
|
||||
|
||||
I always was quite skeptical about adtag. In my POV, a proxy as a fat
|
||||
big connectivity point for hundreds of clients is an illusion. If you
|
||||
work in a censored environment, the first thing that authority does is
|
||||
IP blocking. For us, it means, those big proxies that can benefit from
|
||||
having a pinned channel are going to be blocked in a minute.
|
||||
|
||||
Proxy has to be intimate. It has to be shared within a small group as
|
||||
a family or maybe your college friends. It has to have a small number
|
||||
of connections and never publicly announced its presence. It has to fly
|
||||
under the radar. If the proxy is detected, you need to be able to give
|
||||
a rebirth on a new IP address as soon as possible. I do no think that
|
||||
having some special channel for such a use case makes any sense.
|
||||
|
||||
But other details like replay attack protection, domain fronting,
|
||||
accurate FakeTLS implementation, IP blacklisting, and proxy
|
||||
chaining matter here. If you work in censored perimeter like
|
||||
[GFW](https://en.wikipedia.org/wiki/Great_Firewall)-protected
|
||||
country, you probably want to have an MTPROTO proxy as
|
||||
a frontend that transports traffic via cloaked tunnels
|
||||
made by [Trojan](https://trojan-gfw.github.io/trojan/),
|
||||
[Shadowsocks](https://shadowsocks.org), [v2ray](https://www.v2ray.com/),
|
||||
or [Gost](https://docs.ginuerzh.xyz/gost/). That's why you have to have
|
||||
the support of chaining as a first-class citizen.
|
||||
|
||||
Yes, this is possible and doable with optional adtag support. But the
|
||||
truth is that the MTPROTO proxy for Telegram is just a thing that either
|
||||
work as a normal client (direct mode) or doing some RPC calls in [TL
|
||||
language](https://core.telegram.org/mtproto/TL) (adtag support). I
|
||||
understand the intention of the developers and I understand that they
|
||||
were under high pressure fighting with [RKN](https://rkn.gov.ru/) and
|
||||
doing TON after that. Nothing is ideal. But for the proxy, it means that
|
||||
source code is full of complex non-trivial code which is required only
|
||||
to support a feature that we barely need.
|
||||
|
||||
So, to have a reasonable MTPROTO proxy, adtag support was removed. This
|
||||
is a rare chance in my career where software v2 debloats a previous
|
||||
version. It feels so good :)
|
||||
|
||||
### Version 1 and 2
|
||||
|
||||
I do continue to support both versions 1 and 2. But in a different mode.
|
||||
|
||||
Version 1 is now officially in maintenance mode. It means that I won't
|
||||
make any new features or improvements there. You can consider a feature
|
||||
freeze there. No bugs are going to be fixed there except for critical
|
||||
ones. PRs are welcome though. The goal is to keep it working. It will
|
||||
get some periodical updates like updates to the new Golang version of
|
||||
dependencies version bump, but that's mostly it.
|
||||
|
||||
**If you want to have mtg with _adtag support_, please use version 1**.
|
||||
|
||||
Version 2 is going to have all my love, active support, bug fixing, etc.
|
||||
It is under active development and maintenance.
|
||||
|
||||
This project has several main branches
|
||||
|
||||
1. [`master`](https://github.com/9seconds/mtg/tree/master) branch
|
||||
contains a bleeding edge. It may potentially have some features
|
||||
which will break your source code.
|
||||
2. [`stable`](https://github.com/9seconds/mtg/tree/stable) branch contains
|
||||
dumps of a master branch when we consider it 'stable'. This is a
|
||||
branch you probably want to pick.
|
||||
3. [`v2`](https://github.com/9seconds/mtg/tree/v2) has a development
|
||||
of the v2.x version. In theory, it is the same as `master` but this
|
||||
will change when we have v3.x.
|
||||
4. [`v1`](https://github.com/9seconds/mtg/tree/v1) has a version 1.x.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Download a tool
|
||||
|
||||
#### Download binaries
|
||||
|
||||
Binaries can be downloaded from the release page. Also, you can download
|
||||
docker image.
|
||||
|
||||
For the current version, please download like
|
||||
|
||||
```console
|
||||
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
|
||||
go get github.com/9seconds/mtg/v2
|
||||
```
|
||||
|
||||
#### Build from sources
|
||||
|
||||
```console
|
||||
git clone https://github.com/9seconds/mtg.git
|
||||
cd mtg
|
||||
make static
|
||||
```
|
||||
|
||||
or for the docker image:
|
||||
|
||||
```console
|
||||
make docker
|
||||
```
|
||||
|
||||
# Docker image
|
||||
### Generate secret
|
||||
|
||||
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
|
||||
If you already have a secret in Base64 format or that, which starts with `ee`,
|
||||
you can skip this chapter. Otherwise:
|
||||
|
||||
```console
|
||||
docker pull nineseconds/mtg:latest
|
||||
$ mtg generate-secret google.com
|
||||
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```console
|
||||
docker pull nineseconds/mtg:stable
|
||||
$ mtg generate-secret --hex google.com
|
||||
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||
```
|
||||
|
||||
This secret is a keystone for a proxy and your password for a client.
|
||||
You need to keep it secured.
|
||||
|
||||
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.
|
||||
|
||||
For example, you've bought a VPS from [Digital
|
||||
Ocean](https://www.digitalocean.com/). Then it might be a good idea to
|
||||
generate a secret for _digitalocean.com_ then.
|
||||
|
||||
|
||||
### Simple run mode
|
||||
|
||||
mtg supports 2 modes: simple and normal. Simple mode allows starting
|
||||
proxy with a small subset of configuration options you usually want to
|
||||
modify. This is quite good for oneliners that you can copy-paste and do
|
||||
not bother about external files whatsoever.
|
||||
|
||||
Let's take a look:
|
||||
|
||||
```console
|
||||
docker pull nineseconds/mtg:0.10
|
||||
Usage: mtg simple-run <bind-to> <secret>
|
||||
|
||||
Run proxy without config file.
|
||||
|
||||
Arguments:
|
||||
<bind-to> A host:port to bind proxy to.
|
||||
<secret> Proxy secret.
|
||||
|
||||
Flags:
|
||||
-h, --help Show context-sensitive help.
|
||||
-v, --version Print version.
|
||||
|
||||
-d, --debug Run in debug mode.
|
||||
-c, --concurrency=8192 Max number of concurrent connection to proxy.
|
||||
-b, --tcp-buffer="4KB" Size of TCP buffer to use.
|
||||
-i, --prefer-ip="prefer-ipv6" IP preference. By default we prefer IPv6 with fallback to IPv4.
|
||||
-p, --domain-fronting-port=443 A port to access for domain fronting.
|
||||
-n, --doh-ip=9.9.9.9 IP address of DNS-over-HTTP to use.
|
||||
-t, --timeout=10s Network timeout to use
|
||||
-a, --antireplay-cache-size="1MB" A size of anti-replay cache to use.
|
||||
```
|
||||
|
||||
# Ansible role
|
||||
|
||||
You can find unofficial Ansible role for mtg here: https://github.com/rlex/ansible-role-mtg
|
||||
Also, there is another project on Ansible Galaxy: https://galaxy.ansible.com/ivansible/lin_mtproxy
|
||||
|
||||
# Configuration
|
||||
|
||||
To run this tool you need to configure as less as possible. Telegram
|
||||
clients support 3 different secret types:
|
||||
|
||||
* Simple - basically, it is just a flow of frames ciphered by AES-CTR stream
|
||||
cipher.
|
||||
* Secured - the same stream as simple but with some random noise to prevent
|
||||
statistical analysis of traffic flow.
|
||||
* FakeTLS - this mode envelops telegram stream in TLS so it looks (in theory)
|
||||
the same as any TLS1.3 traffic from DPI point of view.
|
||||
|
||||
If you do not have preferences, go with FakeTLS or at least secured.
|
||||
Simple mode is a little bit naive and traffic flow can be easily
|
||||
identified as Telegram one.
|
||||
|
||||
Unlike the rest of implementation, mtg is quite strict about the
|
||||
execution mode: if you run a proxy instance with FakeTLS secret, you
|
||||
can't connect to it with simple or secured clients. You can't connect
|
||||
to the proxy with secured secret with FakeTLS key. It forces one mode
|
||||
of working. So, unfortunately, there is no way how to connect to the
|
||||
deployed proxy with another secret (if you know how to construct and
|
||||
convert them). But at the same time, old clients can't connect so they
|
||||
won't expose the type of the service.
|
||||
|
||||
First, you need to generate a secret:
|
||||
So, if you want to startup a proxy with CLI only, you can do something like
|
||||
|
||||
```console
|
||||
$ mtg generate-secret simple
|
||||
52a493bdfb90eea55739eabff2d92a14
|
||||
$ mtg simple-run -n 1.1.1.1 -t 30s -a 512kib 127.0.0.1:3128 7hBO-dCS4EBzenlKbdLFxyNnb29nbGUuY29t
|
||||
```
|
||||
|
||||
The rest of the configuration will be taken from default values. But
|
||||
a simple run is fine if you do not have any special requirements or
|
||||
granular tuning. If you want it, please checkout the configuration
|
||||
files.
|
||||
|
||||
### 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
|
||||
$ mtg generate-secret secured
|
||||
ddf05fb7acb549be047a7c585116581418
|
||||
$ 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 you can run a docker image
|
||||
|
||||
```console
|
||||
$ mtg generate-secret -c google.com tls
|
||||
ee852380f362a09343efb4690c4e17862e676f6f676c652e636f6d
|
||||
docker run -d -v /etc/mtg.toml:/config.toml -p 443:3128 --restart=unless-stopped nineseconds/mtg:2
|
||||
```
|
||||
|
||||
Or, if you prefer docker:
|
||||
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.
|
||||
|
||||
### Access a proxy
|
||||
|
||||
Now you can generate some useful links:
|
||||
|
||||
```console
|
||||
$ docker run --rm nineseconds/mtg generate-secret tls -c bing.com
|
||||
eedf71035a8ed48a623d8e83e66aec4d0562696e672e636f6d
|
||||
$ 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": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Antireplay cache
|
||||
## Metrics
|
||||
|
||||
To prevent replay attacks, we have internal storage of first frames
|
||||
messages for connected clients. These frames are generated randomly
|
||||
by design and we have the negligible possibility of duplication
|
||||
(probability is 1/(2^64)) but it could be quite effective to prevent
|
||||
replays.
|
||||
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.
|
||||
|
||||
It is possible to disable this cache. To do that, please explicitly set
|
||||
its size to 0.
|
||||
Here goes a list of metrics with their types but without a prefix.
|
||||
|
||||
| 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. |
|
||||
|
||||
## FakeTLS
|
||||
Tag meaning:
|
||||
|
||||
If you run this a proxy in faketls mode, this proxy will try to hide
|
||||
itself cloaking a host provided as a part of the generated secret. It
|
||||
means that if you cloak google.com then you can curl this proxy and
|
||||
you'll get a google.com response back.
|
||||
|
||||
mtg proxies L3 traffic. In other words, only TCP, without interfering in
|
||||
TLS, HTTP or any other high-level protocol.
|
||||
|
||||
|
||||
## Environment variables
|
||||
|
||||
It is possible to configure this tool using environment variables. You
|
||||
can configure any flag but not secret or adtag. Here is the list of
|
||||
supported environment variables:
|
||||
|
||||
| Environment variable | Corresponding flags | Default value | Description |
|
||||
|-------------------------------|------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `MTG_DEBUG` | `-d`, `--debug` | `false` | Run in debug mode. Usually, you need to run in this mode only if you develop this tool or its maintainer is asking you to provide logs with such verbosity. |
|
||||
| `MTG_VERBOSE` | `-v`, `--verbose` | `false` | Run in verbose mode. This is way less chatty than debug mode. |
|
||||
| `MTG_BIND` | `-b`, `--bind` | `0.0.0.0:3128` | Which host/port pair should we bind to (listen on). |
|
||||
| `MTG_IPV4` | `-4`, `--public-ipv4` | [Autodetect](https://ifconfig.co) | IPv4 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv4 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. |
|
||||
| `MTG_IPV6` | `-6`, `--public-ipv6` | [Autodetect](https://ifconfig.co) | IPv6 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv6 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. |
|
||||
| `MTG_STATS_BIND` | `-t`, `--stats-bind` | `127.0.0.1:3129` | Which hist:port should we bind the internal statistics HTTP server (Prometheus). |
|
||||
| `MTG_STATS_NAMESPACE` | `--stats-namespace` | `mtg` | Which namespace should be used for prometheus metrics. |
|
||||
| `MTG_STATSD_ADDR` | `--statsd-addr` | | host:port of statsd service. No defaults, by default we do not send anything there. |
|
||||
| `MTG_STATSD_PREFIX` | `--statsd-prefix` | `mtg` | Which bucket prefix we should use. For example, if you set `mtg`, then metric `traffic.ingress` would be send as `mtg.traffic.ingress`. |
|
||||
| `MTG_STATSD_TAGS_FORMAT` | `--statsd-tags-format` | | Which tags format we should use. By default, we are using default vanilla statsd tags format but if you want to send directly to InfluxDB or Datadog, please specify it there. Possible options are `influxdb` and `datadog`. |
|
||||
| `MTG_STATSD_TAGS` | `--statsd-tags` | | Which tags should we send to statsd with our metrics. Please specify them as `key=value` pairs. |
|
||||
| `MTG_BUFFER_WRITE` | `-w`, `--write-buffer` | `32KB` | The size of TCP write buffer in bytes. Write buffer is the buffer for messages which are going from client to Telegram. |
|
||||
| `MTG_BUFFER_READ` | `-r`, `--read-buffer` | `32KB` | The size of TCP read buffer in bytes. Read buffer is the buffer for messages from Telegram to client. |
|
||||
| `MTG_ANTIREPLAY_MAXSIZE` | `--anti-replay-max-size` | `128MB` | Max size of antireplay cache. |
|
||||
| `MTG_CLOAK_PORT` | `--cloak-port` | `443` | Which port we should use to connect to cloaked host in FakeTLS mode. |
|
||||
| `MTG_MULTIPLEX_PERCONNECTION` | `--multiplex-per-connection` | `50` | How many client connections can share a single Telegram connection in adtag mode |
|
||||
| `MTG_NTP_SERVERS` | `--ntp-server` | default pool | A list of NTP servers to use. |
|
||||
| `MTG_PREFER_DIRECT_IP` | `--prefer-ip` | `ipv6` | Which IP protocol to prefer if possible. Works mostly in direct mode. |
|
||||
|
||||
|
||||
Usually you want to modify only read/write buffer sizes. If you feel
|
||||
that proxy is slow, try to increase both sizes giving more priority to
|
||||
read buffer.
|
||||
|
||||
Unfortunately, MTPROTO proxy protocol does not allow us to use splice
|
||||
or any other neat tricks how to eliminate the need of copying data into
|
||||
userspace.
|
||||
|
||||
# How to run the tool
|
||||
|
||||
Now run the tool:
|
||||
|
||||
```console
|
||||
$ mtg run <secret>
|
||||
```
|
||||
|
||||
How to run the tool with ADTag:
|
||||
|
||||
```console
|
||||
$ mtg run <secret> <adtag>
|
||||
```
|
||||
|
||||
This tool will listen on port 3128 by default with the given secret.
|
||||
|
||||
|
||||
# oneliner to run this proxy
|
||||
|
||||
Please ensure that docker is installed. After that just execute
|
||||
|
||||
```console
|
||||
curl -sfL --compressed https://raw.githubusercontent.com/9seconds/mtg/master/run.sh | bash
|
||||
```
|
||||
|
||||
|
||||
# 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` | number | The number of active connections. |
|
||||
| `telegram_connections` | number | The number of active telegram connections. |
|
||||
| `crashes` | number | An amount of crashes in client handlers. |
|
||||
| `traffic.egress` | bytes | Traffic from the start of application. |
|
||||
| `replay_attacks` | number | The number of prevented replay attacks. |
|
||||
|
||||
All metrics are prefixed with given prefix. Default prefix is `mtg`.
|
||||
Also, metrics provide tags (ipv4/ipv6, dc indexes etc).
|
||||
|
||||
|
||||
# Prometheus integration
|
||||
|
||||
[Prometheus](https://prometheus.io) integration comes out of
|
||||
the box, you do not need to setup anything special.
|
||||
|
||||
|
||||
# Upgrade to 1.0
|
||||
|
||||
Version 1.0 breaks compatibility with previous versions so please read
|
||||
this chapter carefully:
|
||||
|
||||
1. mtg now uses subcommands. Please use `mtg run` instead of just
|
||||
`mtg` to run a proxy.
|
||||
2. Options which set host and port separately were removed in a
|
||||
favor of fused `host:port` options.
|
||||
3. Own stats server was removed. Prometheus endpoint is moved to
|
||||
default stats endpoint.
|
||||
4. It is possible to connect to this proxy only with a secret which
|
||||
was used to run it. So, no backward compatibility of clients.
|
||||
5. Multiplexing involves connectivity with middle proxies and involves
|
||||
the most complex code path of this proxy. To avoid potential bugs,
|
||||
we still recommend using direct mode.
|
||||
| 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. |
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package antireplay
|
||||
|
||||
import "github.com/VictoriaMetrics/fastcache"
|
||||
|
||||
var (
|
||||
prefixObfuscated2 = []byte{0x00}
|
||||
prefixTLS = []byte{0x01}
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
data *fastcache.Cache
|
||||
}
|
||||
|
||||
func (c cache) AddObfuscated2(data []byte) {
|
||||
c.data.Set(keyObfuscated2(data), nil)
|
||||
}
|
||||
|
||||
func (c cache) AddTLS(data []byte) {
|
||||
c.data.Set(keyTLS(data), nil)
|
||||
}
|
||||
|
||||
func (c cache) HasObfuscated2(data []byte) bool {
|
||||
return c.data.Has(keyObfuscated2(data))
|
||||
}
|
||||
|
||||
func (c cache) HasTLS(data []byte) bool {
|
||||
return c.data.Has(keyTLS(data))
|
||||
}
|
||||
|
||||
func keyObfuscated2(data []byte) []byte {
|
||||
return append(prefixObfuscated2, data...)
|
||||
}
|
||||
|
||||
func keyTLS(data []byte) []byte {
|
||||
return append(prefixTLS, data...)
|
||||
}
|
||||
+13
-26
@@ -1,30 +1,17 @@
|
||||
// Antireplay package has cache implementations that are effective
|
||||
// against replay attacks.
|
||||
//
|
||||
// To understand more about replay attacks, please read documentation
|
||||
// for mtglib.AntiReplayCache interface. This package has a list of some
|
||||
// implementations of this interface.
|
||||
package antireplay
|
||||
|
||||
import (
|
||||
"sync"
|
||||
const (
|
||||
// DefaultStableBloomFilterMaxSize is a recommended byte size for a
|
||||
// stable bloom filter.
|
||||
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
// DefaultStableBloomFilterErrorRate is a recommended default error
|
||||
// rate for a stable bloom filter.
|
||||
DefaultStableBloomFilterErrorRate = 0.001
|
||||
)
|
||||
|
||||
type CacheInterface interface {
|
||||
AddObfuscated2([]byte)
|
||||
AddTLS([]byte)
|
||||
HasObfuscated2([]byte) bool
|
||||
HasTLS([]byte) bool
|
||||
}
|
||||
|
||||
var (
|
||||
Cache CacheInterface
|
||||
initOnce sync.Once
|
||||
)
|
||||
|
||||
func Init() {
|
||||
initOnce.Do(func() {
|
||||
if config.C.AntiReplayMaxSize == 0 {
|
||||
Cache = nilCache{}
|
||||
} else {
|
||||
Cache = cache{fastcache.New(config.C.AntiReplayMaxSize)}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package antireplay
|
||||
|
||||
type nilCache struct{}
|
||||
|
||||
func (n nilCache) AddObfuscated2(_ []byte) {}
|
||||
func (n nilCache) AddTLS(_ []byte) {}
|
||||
func (n nilCache) HasObfuscated2(_ []byte) bool { return false }
|
||||
func (n nilCache) HasTLS(_ []byte) bool { return false }
|
||||
@@ -0,0 +1,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,26 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
)
|
||||
|
||||
func Generate(secretType, hostname string) {
|
||||
data := make([]byte, config.SimpleSecretLength)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
secret := hex.EncodeToString(data)
|
||||
|
||||
switch secretType {
|
||||
case "simple":
|
||||
PrintStdout(secret)
|
||||
case "secured":
|
||||
PrintStdout("dd" + secret)
|
||||
default:
|
||||
PrintStdout("ee" + secret + hex.EncodeToString([]byte(hostname)))
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/antireplay"
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/faketls"
|
||||
"github.com/9seconds/mtg/hub"
|
||||
"github.com/9seconds/mtg/ntp"
|
||||
"github.com/9seconds/mtg/obfuscated2"
|
||||
"github.com/9seconds/mtg/proxy"
|
||||
"github.com/9seconds/mtg/stats"
|
||||
"github.com/9seconds/mtg/telegram"
|
||||
"github.com/9seconds/mtg/utils"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func Proxy() error { // nolint: funlen,cyclop
|
||||
ctx := utils.GetSignalContext()
|
||||
|
||||
atom := zap.NewAtomicLevel()
|
||||
|
||||
switch {
|
||||
case config.C.Debug:
|
||||
atom.SetLevel(zapcore.DebugLevel)
|
||||
case config.C.Verbose:
|
||||
atom.SetLevel(zapcore.InfoLevel)
|
||||
default:
|
||||
atom.SetLevel(zapcore.ErrorLevel)
|
||||
}
|
||||
|
||||
encoderCfg := zap.NewProductionEncoderConfig()
|
||||
logger := zap.New(zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderCfg),
|
||||
zapcore.Lock(os.Stderr),
|
||||
atom,
|
||||
))
|
||||
|
||||
zap.ReplaceGlobals(logger)
|
||||
defer logger.Sync() // nolint: errcheck
|
||||
|
||||
if err := config.InitPublicAddress(ctx); err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
|
||||
zap.S().Debugw("Configuration", "config", config.Printable())
|
||||
|
||||
if config.C.MiddleProxyMode() {
|
||||
zap.S().Infow("Use middle proxy connection to Telegram")
|
||||
|
||||
diff, err := ntp.Fetch()
|
||||
if err != nil {
|
||||
Fatal("Cannot fetch time data from NTP")
|
||||
}
|
||||
|
||||
if diff > time.Second {
|
||||
Fatal("Your local time is skewed and drift is bigger than a second. Please sync your time.")
|
||||
}
|
||||
|
||||
go ntp.AutoUpdate()
|
||||
} else {
|
||||
zap.S().Infow("Use direct connection to Telegram")
|
||||
}
|
||||
|
||||
PrintJSONStdout(config.GetURLs())
|
||||
|
||||
if err := stats.Init(ctx); err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
|
||||
antireplay.Init()
|
||||
telegram.Init()
|
||||
hub.Init(ctx)
|
||||
|
||||
proxyListener, err := net.Listen("tcp", config.C.Bind.String())
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
proxyListener.Close()
|
||||
}()
|
||||
|
||||
app := &proxy.Proxy{
|
||||
Logger: zap.S().Named("proxy"),
|
||||
Context: ctx,
|
||||
ClientProtocolMaker: obfuscated2.MakeClientProtocol,
|
||||
}
|
||||
if config.C.SecretMode == config.SecretModeTLS {
|
||||
app.ClientProtocolMaker = faketls.MakeClientProtocol
|
||||
}
|
||||
|
||||
app.Serve(proxyListener)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Fatal(arg interface{}) {
|
||||
if value, ok := arg.(error); ok {
|
||||
arg = fmt.Errorf("fatal error: %+v", value) // nolint: errorlint
|
||||
}
|
||||
|
||||
PrintStderr(arg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func PrintStderr(args ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, args...)
|
||||
}
|
||||
|
||||
func PrintStdout(args ...interface{}) {
|
||||
fmt.Println(args...) // nolint: forbidigo
|
||||
}
|
||||
|
||||
func PrintJSONStderr(data interface{}) {
|
||||
printJSON(os.Stderr, data)
|
||||
}
|
||||
|
||||
func PrintJSONStdout(data interface{}) {
|
||||
printJSON(os.Stdout, data)
|
||||
}
|
||||
|
||||
func printJSON(writer io.Writer, data interface{}) {
|
||||
encoder := json.NewEncoder(writer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
statsd "github.com/smira/go-statsd"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SecretMode uint8
|
||||
|
||||
func (s SecretMode) String() string {
|
||||
switch s {
|
||||
case SecretModeSimple:
|
||||
return "simple"
|
||||
case SecretModeSecured:
|
||||
return "secured"
|
||||
case SecretModeTLS:
|
||||
return "tls"
|
||||
}
|
||||
|
||||
return "tls"
|
||||
}
|
||||
|
||||
const (
|
||||
SecretModeSimple SecretMode = iota
|
||||
SecretModeSecured
|
||||
SecretModeTLS
|
||||
)
|
||||
|
||||
type PreferIP uint8
|
||||
|
||||
const (
|
||||
PreferIPv4 PreferIP = iota
|
||||
PreferIPv6
|
||||
)
|
||||
|
||||
const SimpleSecretLength = 16
|
||||
|
||||
type OptionType uint8
|
||||
|
||||
const (
|
||||
OptionTypeDebug OptionType = iota
|
||||
OptionTypeVerbose
|
||||
|
||||
OptionTypePreferIP
|
||||
|
||||
OptionTypeBind
|
||||
OptionTypePublicIPv4
|
||||
OptionTypePublicIPv6
|
||||
|
||||
OptionTypeStatsBind
|
||||
OptionTypeStatsNamespace
|
||||
OptionTypeStatsdAddress
|
||||
OptionTypeStatsdTagsFormat
|
||||
OptionTypeStatsdTags
|
||||
|
||||
OptionTypeWriteBufferSize
|
||||
OptionTypeReadBufferSize
|
||||
|
||||
OptionTypeCloakPort
|
||||
|
||||
OptionTypeAntiReplayMaxSize
|
||||
|
||||
OptionTypeMultiplexPerConnection
|
||||
|
||||
OptionTypeNTPServers
|
||||
|
||||
OptionTypeSecret
|
||||
OptionTypeAdtag
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Bind *net.TCPAddr `json:"bind"`
|
||||
PublicIPv4 *net.TCPAddr `json:"public_ipv4"` // nolint: tagliatelle
|
||||
PublicIPv6 *net.TCPAddr `json:"public_ipv6"` // nolint: tagliatelle
|
||||
StatsBind *net.TCPAddr `json:"stats_bind"` // nolint: tagliatelle
|
||||
StatsdAddr *net.TCPAddr `json:"stats_addr"` // nolint: tagliatelle
|
||||
StatsdTagsFormat *statsd.TagFormat `json:"statsd_tags_format"` // nolint: tagliatelle
|
||||
|
||||
StatsNamespace string `json:"stats_namespace"` // nolint: tagliatelle
|
||||
CloakHost string `json:"cloak_host"` // nolint: tagliatelle
|
||||
StatsdTags map[string]string `json:"statsd_tags"` // nolint: tagliatelle
|
||||
|
||||
WriteBuffer int `json:"write_buffer"` // nolint: tagliatelle
|
||||
ReadBuffer int `json:"read_buffer"` // nolint: tagliatelle
|
||||
CloakPort int `json:"cloak_port"` // nolint: tagliatelle
|
||||
|
||||
AntiReplayMaxSize int `json:"anti_replay_max_size"` // nolint: tagliatelle
|
||||
|
||||
MultiplexPerConnection int `json:"multiplex_per_connection"` // nolint: tagliatelle
|
||||
|
||||
Debug bool `json:"debug"`
|
||||
Verbose bool `json:"verbose"`
|
||||
SecretMode SecretMode `json:"secret_mode"` // nolint: tagliatelle
|
||||
PreferIP PreferIP `json:"prefer_ip"` // nolint: tagliatelle
|
||||
NTPServers []string `json:"ntp_servers"` // nolint: tagliatelle
|
||||
|
||||
Secret []byte `json:"secret"`
|
||||
AdTag []byte `json:"adtag"`
|
||||
}
|
||||
|
||||
func (c *Config) ClientReadBuffer() int {
|
||||
return c.ReadBuffer
|
||||
}
|
||||
|
||||
func (c *Config) ClientWriteBuffer() int {
|
||||
return c.WriteBuffer
|
||||
}
|
||||
|
||||
func (c *Config) MiddleProxyMode() bool {
|
||||
return len(c.AdTag) > 0
|
||||
}
|
||||
|
||||
func (c *Config) ProxyReadBuffer() int {
|
||||
value := c.ReadBuffer
|
||||
|
||||
if c.MiddleProxyMode() {
|
||||
value = c.adjustProxyValue(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *Config) ProxyWriteBuffer() int {
|
||||
value := c.WriteBuffer
|
||||
|
||||
if c.MiddleProxyMode() {
|
||||
value = c.adjustProxyValue(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *Config) adjustProxyValue(value int) int {
|
||||
if c.MultiplexPerConnection == 0 {
|
||||
return value
|
||||
}
|
||||
|
||||
fvalue := float64(value)
|
||||
|
||||
newValue := fvalue * 2 * math.Log(float64(c.MultiplexPerConnection))
|
||||
newValue = math.Ceil(newValue)
|
||||
newValue = math.Max(fvalue, newValue)
|
||||
|
||||
return int(newValue)
|
||||
}
|
||||
|
||||
type Opt struct {
|
||||
Option OptionType
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
var C = Config{}
|
||||
|
||||
func Init(options ...Opt) error { // nolint: gocyclo, funlen, cyclop
|
||||
for _, opt := range options {
|
||||
switch opt.Option {
|
||||
case OptionTypeDebug:
|
||||
C.Debug = opt.Value.(bool) // nolint: forcetypeassert
|
||||
case OptionTypeVerbose:
|
||||
C.Verbose = opt.Value.(bool) // nolint: forcetypeassert
|
||||
case OptionTypePreferIP:
|
||||
value := opt.Value.(string) // nolint: forcetypeassert
|
||||
switch value {
|
||||
case "ipv4":
|
||||
C.PreferIP = PreferIPv4
|
||||
case "ipv6":
|
||||
C.PreferIP = PreferIPv6
|
||||
default:
|
||||
return fmt.Errorf("incorrect direct IP mode %s", value)
|
||||
}
|
||||
case OptionTypeBind:
|
||||
C.Bind = opt.Value.(*net.TCPAddr) // nolint: forcetypeassert
|
||||
case OptionTypePublicIPv4:
|
||||
C.PublicIPv4 = opt.Value.(*net.TCPAddr) // nolint: forcetypeassert
|
||||
if C.PublicIPv4 == nil {
|
||||
C.PublicIPv4 = &net.TCPAddr{}
|
||||
}
|
||||
case OptionTypePublicIPv6:
|
||||
C.PublicIPv6 = opt.Value.(*net.TCPAddr) // nolint: forcetypeassert
|
||||
if C.PublicIPv6 == nil {
|
||||
C.PublicIPv6 = &net.TCPAddr{}
|
||||
}
|
||||
case OptionTypeStatsBind:
|
||||
C.StatsBind = opt.Value.(*net.TCPAddr) // nolint: forcetypeassert
|
||||
case OptionTypeStatsNamespace:
|
||||
C.StatsNamespace = opt.Value.(string) // nolint: forcetypeassert
|
||||
case OptionTypeStatsdAddress:
|
||||
C.StatsdAddr = opt.Value.(*net.TCPAddr) // nolint: forcetypeassert
|
||||
case OptionTypeStatsdTagsFormat:
|
||||
value := opt.Value.(string) // nolint: forcetypeassert
|
||||
switch value {
|
||||
case "datadog":
|
||||
C.StatsdTagsFormat = statsd.TagFormatDatadog
|
||||
case "influxdb":
|
||||
C.StatsdTagsFormat = statsd.TagFormatInfluxDB
|
||||
default:
|
||||
return fmt.Errorf("incorrect statsd tag %s", value)
|
||||
}
|
||||
case OptionTypeStatsdTags:
|
||||
C.StatsdTags = opt.Value.(map[string]string) // nolint: forcetypeassert
|
||||
case OptionTypeWriteBufferSize:
|
||||
C.WriteBuffer = int(opt.Value.(units.Base2Bytes))
|
||||
case OptionTypeReadBufferSize:
|
||||
C.ReadBuffer = int(opt.Value.(units.Base2Bytes))
|
||||
case OptionTypeCloakPort:
|
||||
C.CloakPort = int(opt.Value.(uint16))
|
||||
case OptionTypeAntiReplayMaxSize:
|
||||
C.AntiReplayMaxSize = int(opt.Value.(units.Base2Bytes))
|
||||
case OptionTypeMultiplexPerConnection:
|
||||
C.MultiplexPerConnection = int(opt.Value.(uint))
|
||||
case OptionTypeNTPServers:
|
||||
C.NTPServers = opt.Value.([]string) // nolint: forcetypeassert
|
||||
if len(C.NTPServers) == 0 {
|
||||
return errors.New("ntp server list is empty")
|
||||
}
|
||||
case OptionTypeSecret:
|
||||
C.Secret = opt.Value.([]byte) // nolint: forcetypeassert
|
||||
case OptionTypeAdtag:
|
||||
C.AdTag = opt.Value.([]byte) // nolint: forcetypeassert
|
||||
default:
|
||||
return fmt.Errorf("unknown tag %v", opt.Option)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(C.Secret) == 1+SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xdd}):
|
||||
C.SecretMode = SecretModeSecured
|
||||
C.Secret = bytes.TrimPrefix(C.Secret, []byte{0xdd})
|
||||
case len(C.Secret) > SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xee}):
|
||||
C.SecretMode = SecretModeTLS
|
||||
secret := bytes.TrimPrefix(C.Secret, []byte{0xee})
|
||||
C.Secret = secret[:SimpleSecretLength]
|
||||
C.CloakHost = string(secret[SimpleSecretLength:])
|
||||
case len(C.Secret) == SimpleSecretLength:
|
||||
C.SecretMode = SecretModeSimple
|
||||
default:
|
||||
return errors.New("incorrect secret")
|
||||
}
|
||||
|
||||
if C.MultiplexPerConnection == 0 {
|
||||
return errors.New("cannot use 0 clients per connection for multiplexing")
|
||||
}
|
||||
|
||||
if C.CloakHost != "" {
|
||||
if _, err := net.LookupHost(C.CloakHost); err != nil {
|
||||
zap.S().Warnw("Cannot resolve address of host", "hostname", C.CloakHost, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitPublicAddress(ctx context.Context) error {
|
||||
if C.PublicIPv4.Port == 0 {
|
||||
C.PublicIPv4.Port = C.Bind.Port
|
||||
}
|
||||
|
||||
if C.PublicIPv6.Port == 0 {
|
||||
C.PublicIPv6.Port = C.Bind.Port
|
||||
}
|
||||
|
||||
foundAddress := C.PublicIPv4.IP != nil || C.PublicIPv6.IP != nil
|
||||
|
||||
if C.PublicIPv4.IP == nil {
|
||||
ip, err := getGlobalIPv4(ctx)
|
||||
if err != nil {
|
||||
zap.S().Warnw("Cannot resolve public address", "error", err)
|
||||
} else {
|
||||
C.PublicIPv4.IP = ip
|
||||
foundAddress = true
|
||||
}
|
||||
}
|
||||
|
||||
if C.PublicIPv6.IP == nil {
|
||||
ip, err := getGlobalIPv6(ctx)
|
||||
if err != nil {
|
||||
zap.S().Warnw("Cannot resolve public address", "error", err)
|
||||
} else {
|
||||
C.PublicIPv6.IP = ip
|
||||
foundAddress = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundAddress {
|
||||
return errors.New("cannot resolve any public address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Printable() interface{} {
|
||||
data, err := json.Marshal(C)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rv := map[string]interface{}{}
|
||||
if err := json.Unmarshal(data, &rv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ifconfigAddress = "https://ifconfig.co/ip"
|
||||
ifconfigTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
func getGlobalIPv4(ctx context.Context) (net.IP, error) {
|
||||
ip, err := fetchIP(ctx, "tcp4")
|
||||
if err != nil || ip.To4() == nil {
|
||||
return nil, fmt.Errorf("cannot find public ipv4 address: %w", err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func getGlobalIPv6(ctx context.Context) (net.IP, error) {
|
||||
ip, err := fetchIP(ctx, "tcp6")
|
||||
if err != nil || ip.To4() != nil {
|
||||
return nil, fmt.Errorf("cannot find public ipv6 address: %w", err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func fetchIP(ctx context.Context, network string) (net.IP, error) {
|
||||
dialer := &net.Dialer{FallbackDelay: -1}
|
||||
client := &http.Client{
|
||||
Jar: nil,
|
||||
Timeout: ifconfigTimeout,
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, addr) // nolint: wrapcheck
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ifconfigAddress, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create a request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
io.Copy(ioutil.Discard, resp.Body) // nolint: errcheck
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot perform a request: %w", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDataBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read response body: %w", err)
|
||||
}
|
||||
|
||||
respData := strings.TrimSpace(string(respDataBytes))
|
||||
|
||||
ip := net.ParseIP(respData)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("ifconfig.co returns incorrect IP %s", respData)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type URLs struct {
|
||||
TG string `json:"tg_url"` // nolint: tagliatelle
|
||||
TMe string `json:"tme_url"` // nolint: tagliatelle
|
||||
TGQRCode string `json:"tg_qrcode"` // nolint: tagliatelle
|
||||
TMeQRCode string `json:"tme_qrcode"` // nolint: tagliatelle
|
||||
}
|
||||
|
||||
type IPURLs struct {
|
||||
IPv4 *URLs `json:"ipv4,omitempty"`
|
||||
IPv6 *URLs `json:"ipv6,omitempty"`
|
||||
BotSecret string `json:"secret_for_mtproxybot"` // nolint: tagliatelle
|
||||
}
|
||||
|
||||
func GetURLs() (urls IPURLs) {
|
||||
secret := ""
|
||||
|
||||
switch C.SecretMode {
|
||||
case SecretModeSimple:
|
||||
secret = hex.EncodeToString(C.Secret)
|
||||
case SecretModeSecured:
|
||||
secret = "dd" + hex.EncodeToString(C.Secret)
|
||||
case SecretModeTLS:
|
||||
secret = "ee" + hex.EncodeToString(C.Secret) + hex.EncodeToString([]byte(C.CloakHost))
|
||||
}
|
||||
|
||||
if C.PublicIPv4.IP != nil {
|
||||
urls.IPv4 = makeURLs(C.PublicIPv4, secret)
|
||||
}
|
||||
|
||||
if C.PublicIPv6.IP != nil {
|
||||
urls.IPv6 = makeURLs(C.PublicIPv6, secret)
|
||||
}
|
||||
|
||||
urls.BotSecret = hex.EncodeToString(C.Secret)
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
func makeURLs(addr *net.TCPAddr, secret string) *URLs {
|
||||
urls := &URLs{}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("server", addr.IP.String())
|
||||
values.Set("port", strconv.Itoa(addr.Port))
|
||||
values.Set("secret", secret)
|
||||
|
||||
return &URLs{
|
||||
TG: makeTGURL(values),
|
||||
TMe: makeTMeURL(values),
|
||||
TGQRCode: makeQRCodeURL(urls.TG),
|
||||
TMeQRCode: makeQRCodeURL(urls.TMe),
|
||||
}
|
||||
}
|
||||
|
||||
func makeTGURL(values url.Values) string {
|
||||
tgURL := url.URL{
|
||||
Scheme: "tg",
|
||||
Host: "proxy",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return tgURL.String()
|
||||
}
|
||||
|
||||
func makeTMeURL(values url.Values) string {
|
||||
tMeURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "t.me",
|
||||
Path: "proxy",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return tMeURL.String()
|
||||
}
|
||||
|
||||
func makeQRCodeURL(data string) string {
|
||||
qr := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.qrserver.com",
|
||||
Path: "v1/create-qr-code",
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("qzone", "4")
|
||||
values.Set("format", "svg")
|
||||
values.Set("data", data)
|
||||
qr.RawQuery = values.Encode()
|
||||
|
||||
return qr.String()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
type ConnectionAcks struct {
|
||||
Simple bool
|
||||
Quick bool
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
type DC int16
|
||||
|
||||
const DCDefaultIdx DC = 1
|
||||
@@ -1,24 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
const ConnIDLength = 8
|
||||
|
||||
type ConnID [ConnIDLength]byte
|
||||
|
||||
func (c ConnID) String() string {
|
||||
return hex.EncodeToString(c[:])
|
||||
}
|
||||
|
||||
func NewConnID() ConnID {
|
||||
var id ConnID
|
||||
|
||||
if _, err := rand.Read(id[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
type Packet []byte
|
||||
@@ -1,22 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
type ConnectionProtocol uint8
|
||||
|
||||
func (c ConnectionProtocol) String() string {
|
||||
switch c {
|
||||
case ConnectionProtocolAny:
|
||||
return "any"
|
||||
case ConnectionProtocolIPv4:
|
||||
return "ipv4"
|
||||
case ConnectionProtocolIPv6:
|
||||
return "ipv6"
|
||||
}
|
||||
|
||||
return "ipv6"
|
||||
}
|
||||
|
||||
const (
|
||||
ConnectionProtocolIPv4 ConnectionProtocol = 1
|
||||
ConnectionProtocolIPv6 = ConnectionProtocolIPv4 << 1
|
||||
ConnectionProtocolAny = ConnectionProtocolIPv4 | ConnectionProtocolIPv6
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
type ConnectionType uint8
|
||||
|
||||
const (
|
||||
ConnectionTypeUnknown ConnectionType = iota
|
||||
ConnectionTypeAbridged
|
||||
ConnectionTypeIntermediate
|
||||
ConnectionTypeSecure
|
||||
)
|
||||
|
||||
var (
|
||||
ConnectionTagAbridged = []byte{0xef, 0xef, 0xef, 0xef}
|
||||
ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee}
|
||||
ConnectionTagSecure = []byte{0xdd, 0xdd, 0xdd, 0xdd}
|
||||
)
|
||||
|
||||
func (t ConnectionType) Tag() []byte {
|
||||
switch t {
|
||||
case ConnectionTypeAbridged:
|
||||
return ConnectionTagAbridged
|
||||
case ConnectionTypeIntermediate:
|
||||
return ConnectionTagIntermediate
|
||||
case ConnectionTypeSecure, ConnectionTypeUnknown:
|
||||
return ConnectionTagSecure
|
||||
}
|
||||
|
||||
return ConnectionTagSecure
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Wrap interface {
|
||||
Conn() net.Conn
|
||||
Logger() *zap.SugaredLogger
|
||||
LocalAddr() *net.TCPAddr
|
||||
RemoteAddr() *net.TCPAddr
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
import "io"
|
||||
|
||||
type PacketAckReader interface {
|
||||
Read(*ConnectionAcks) (Packet, error)
|
||||
}
|
||||
|
||||
type PacketAckWriter interface {
|
||||
Write(Packet, *ConnectionAcks) error
|
||||
}
|
||||
|
||||
type PacketAckCloser interface {
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type PacketAckReadCloser interface {
|
||||
PacketAckReader
|
||||
PacketAckCloser
|
||||
}
|
||||
|
||||
type PacketAckWriteCloser interface {
|
||||
PacketAckWriter
|
||||
PacketAckCloser
|
||||
}
|
||||
|
||||
type PacketAckReadWriter interface {
|
||||
PacketAckReader
|
||||
PacketAckWriter
|
||||
}
|
||||
|
||||
type PacketAckReadWriteCloser interface {
|
||||
PacketAckReader
|
||||
PacketAckWriter
|
||||
PacketAckCloser
|
||||
}
|
||||
|
||||
type PacketAckFullReadWriteCloser interface {
|
||||
Wrap
|
||||
PacketAckReadWriteCloser
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
import "io"
|
||||
|
||||
type BasePacketReader interface {
|
||||
Read() (Packet, error)
|
||||
}
|
||||
|
||||
type BasePacketWriter interface {
|
||||
Write(Packet) error
|
||||
}
|
||||
|
||||
type PacketReader interface {
|
||||
Wrap
|
||||
BasePacketReader
|
||||
}
|
||||
|
||||
type PacketWriter interface {
|
||||
Wrap
|
||||
BasePacketWriter
|
||||
}
|
||||
|
||||
type PacketCloser interface {
|
||||
Wrap
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type PacketReadCloser interface {
|
||||
Wrap
|
||||
BasePacketReader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type PacketWriteCloser interface {
|
||||
Wrap
|
||||
BasePacketWriter
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type PacketReadWriter interface {
|
||||
Wrap
|
||||
BasePacketWriter
|
||||
BasePacketReader
|
||||
}
|
||||
|
||||
type PacketReadWriteCloser interface {
|
||||
Wrap
|
||||
BasePacketWriter
|
||||
BasePacketReader
|
||||
io.Closer
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package conntypes
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseStreamReaderWithTimeout interface {
|
||||
ReadTimeout([]byte, time.Duration) (int, error)
|
||||
}
|
||||
|
||||
type BaseStreamWriterWithTimeout interface {
|
||||
WriteTimeout([]byte, time.Duration) (int, error)
|
||||
}
|
||||
|
||||
type StreamReader interface {
|
||||
Wrap
|
||||
io.Reader
|
||||
BaseStreamReaderWithTimeout
|
||||
}
|
||||
|
||||
type StreamWriter interface {
|
||||
Wrap
|
||||
io.Writer
|
||||
BaseStreamWriterWithTimeout
|
||||
}
|
||||
|
||||
type StreamCloser interface {
|
||||
Wrap
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type StreamReadCloser interface {
|
||||
Wrap
|
||||
io.ReadCloser
|
||||
BaseStreamReaderWithTimeout
|
||||
}
|
||||
|
||||
type StreamWriteCloser interface {
|
||||
Wrap
|
||||
io.WriteCloser
|
||||
BaseStreamWriterWithTimeout
|
||||
}
|
||||
|
||||
type StreamReadWriter interface {
|
||||
Wrap
|
||||
io.ReadWriter
|
||||
BaseStreamReaderWithTimeout
|
||||
}
|
||||
|
||||
type StreamReadWriteCloser interface {
|
||||
Wrap
|
||||
io.ReadWriteCloser
|
||||
BaseStreamReaderWithTimeout
|
||||
BaseStreamWriterWithTimeout
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package essentials
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// CloseableReader is a reader interface that can close its reading end.
|
||||
type CloseableReader interface {
|
||||
io.Reader
|
||||
|
||||
CloseRead() error
|
||||
}
|
||||
|
||||
// CloseableWriter is a writer that can close its writing end.
|
||||
type CloseableWriter interface {
|
||||
io.Writer
|
||||
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
// Conn is an extension of net.Conn that can close its ends. This mostly
|
||||
// implies TCP connections.
|
||||
type Conn interface {
|
||||
net.Conn
|
||||
CloseableReader
|
||||
CloseableWriter
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// This is a minimal package that contains _essentials_ of mtglib and its
|
||||
// complimentary packages. This is mostly required to comply some interfaces
|
||||
// between mtglib and its internals to avoid circular dependencies.
|
||||
//
|
||||
// This package should contain only bare minimum and mostly technical.
|
||||
package essentials
|
||||
@@ -0,0 +1,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,222 @@
|
||||
# This is an example of the configuration file for mtg. You actually can
|
||||
# run mtg with it. It starts a proxy on all interfaces with a secret
|
||||
# ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d
|
||||
#
|
||||
# It has all possible options with default values. So, a real world
|
||||
# configuration file should contain only those options you are going to
|
||||
# use. You do not need to enumerate all of them. In other words, each
|
||||
# option here has a default value. If you comment a key-value pair, it
|
||||
# should not make any effect.
|
||||
#
|
||||
# stats is the only exception.
|
||||
|
||||
# Debug starts application in debug mode. It starts to be quite verbose
|
||||
# in output. Actually, the idea is that you run it in debug mode only if
|
||||
# you have any issue.
|
||||
debug = true
|
||||
|
||||
# A secret. Please remember that mtg supports only FakeTLS mode, legacy
|
||||
# simple and secured mode are prohibited. For you it means that secret
|
||||
# should either be base64-encoded or starts with ee.
|
||||
secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d"
|
||||
|
||||
# Host:port pair to run proxy on.
|
||||
bind-to = "0.0.0.0:3128"
|
||||
|
||||
# Defines how many concurrent connections are allowed to this proxy.
|
||||
# All other incoming connections are going to be dropped.
|
||||
concurrency = 8192
|
||||
|
||||
# A size of user-space buffer for TCP to use. Since we do 2 connections,
|
||||
# then we have tcp-buffer * (4 + 2) per each connection: read/write for
|
||||
# each connection + 2 copy buffers to pump the data between sockets.
|
||||
#
|
||||
# Deprecated: this setting is no longer makes any effect.
|
||||
# tcp-buffer = "4kb"
|
||||
|
||||
# Sometimes you want to enforce mtg to use some types of
|
||||
# IP connectivity to Telegram. We have 4 modes:
|
||||
# - prefer-ipv6:
|
||||
# We can use both ipv4 and ipv6 but ipv6 has a preference
|
||||
# - prefer-ipv4:
|
||||
# We can use both ipv4 and ipv6 but ipv4 has a preference
|
||||
# - only-ipv6:
|
||||
# Only ipv6 connectivity is used
|
||||
# - only-ipv4:
|
||||
# Only ipv4 connectivity is used
|
||||
prefer-ip = "prefer-ipv6"
|
||||
|
||||
# FakeTLS uses domain fronting protection. So it needs to know a port to
|
||||
# access.
|
||||
domain-fronting-port = 443
|
||||
|
||||
# FakeTLS can compare timestamps to prevent probes. Each message has
|
||||
# encrypted timestamp. So, mtg can compare this timestamp and decide if
|
||||
# we need to proceed with connection or not.
|
||||
#
|
||||
# Sometimes time can be skewed so we accept all messages within a
|
||||
# time range of this parameter.
|
||||
tolerate-time-skewness = "5s"
|
||||
|
||||
# Telegram has a concept of DC. You can think about DC as a number of a cluster
|
||||
# with a certain purpose. Some clusters serve media, some - messages, some rule
|
||||
# channels and so on. But sometimes unknown DC number is requested by client.
|
||||
# It could be a bug or some global reconfiguration of the Telegram.
|
||||
#
|
||||
# By default, proxy rejects such requests. But it is also possible to fallback
|
||||
# this request to any DC. Telegram works in a way that any DC is able to serve
|
||||
# any request but sacrificing a latency.
|
||||
#
|
||||
# If this setting is disabled (default), mtg will reject a connection.
|
||||
# Otherwise, chose a new DC.
|
||||
allow-fallback-on-unknown-dc = false
|
||||
|
||||
# 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"
|
||||
|
||||
# Allowlist is an opposite to a blocklist. Only those IPs that are coming from
|
||||
# subnets defined in these lists are allowed. All others will be rejected.
|
||||
#
|
||||
# If this feature is disabled, then there won't be any check performed by this
|
||||
# validator. It is possible to combine both blocklist and whitelist.
|
||||
[defense.allowlist]
|
||||
# You can enable/disable this feature.
|
||||
enabled = false
|
||||
# This is a limiter for concurrency. In order to protect website
|
||||
# from overloading, we download files in this number of threads.
|
||||
download-concurrency = 2
|
||||
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
|
||||
# You can provider links here (starts with https:// or http://) or
|
||||
# path to a local file, but in this case it should be absolute.
|
||||
urls = [
|
||||
# "https://iplists.firehol.org/files/firehol_level1.netset",
|
||||
# "/local.file"
|
||||
|
||||
]
|
||||
update-each = "24h"
|
||||
|
||||
# statsd statistics integration.
|
||||
[stats.statsd]
|
||||
# enabled/disabled
|
||||
enabled = false
|
||||
# host:port for UDP endpoint of statsd
|
||||
address = "127.0.0.1:8888"
|
||||
# prefix of metric for statsd
|
||||
metric-prefix = "mtg"
|
||||
# tag format to use
|
||||
# supported values are 'datadog', 'influxdb' and 'graphite'
|
||||
# default format is graphite.
|
||||
tag-format = "datadog"
|
||||
|
||||
# prometheus metrics integration.
|
||||
[stats.prometheus]
|
||||
# enabled/disabled
|
||||
enabled = true
|
||||
# host:port where to start http server for endpoint
|
||||
bind-to = "127.0.0.1:3129"
|
||||
# prefix of http path
|
||||
http-path = "/"
|
||||
# prefix for metrics for prometheus
|
||||
metric-prefix = "mtg"
|
||||
@@ -1,122 +0,0 @@
|
||||
package faketls
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/antireplay"
|
||||
"github.com/9seconds/mtg/config"
|
||||
"github.com/9seconds/mtg/conntypes"
|
||||
"github.com/9seconds/mtg/obfuscated2"
|
||||
"github.com/9seconds/mtg/protocol"
|
||||
"github.com/9seconds/mtg/stats"
|
||||
"github.com/9seconds/mtg/tlstypes"
|
||||
"github.com/9seconds/mtg/wrappers/stream"
|
||||
)
|
||||
|
||||
type ClientProtocol struct {
|
||||
obfuscated2.ClientProtocol
|
||||
}
|
||||
|
||||
func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) {
|
||||
rewinded := stream.NewRewind(socket)
|
||||
bufferedReader := bufio.NewReader(rewinded)
|
||||
|
||||
for _, expected := range faketlsStartBytes {
|
||||
if actual, err := bufferedReader.ReadByte(); err != nil || actual != expected {
|
||||
rewinded.Rewind()
|
||||
c.cloakHost(rewinded)
|
||||
|
||||
return nil, errors.New("failed first bytes of tls handshake")
|
||||
}
|
||||
}
|
||||
|
||||
rewinded.Rewind()
|
||||
rewinded = stream.NewRewind(rewinded)
|
||||
|
||||
if err := c.tlsHandshake(rewinded); err != nil {
|
||||
rewinded.Rewind()
|
||||
c.cloakHost(rewinded)
|
||||
|
||||
return nil, fmt.Errorf("failed tls handshake: %w", err)
|
||||
}
|
||||
|
||||
conn := stream.NewFakeTLS(socket)
|
||||
|
||||
conn, err := c.ClientProtocol.Handshake(conn)
|
||||
if err != nil {
|
||||
return nil, err // nolint: wrapcheck
|
||||
}
|
||||
|
||||
return conn, err // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (c *ClientProtocol) tlsHandshake(conn io.ReadWriter) error {
|
||||
helloRecord, err := tlstypes.ReadRecord(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read initial record: %w", err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
helloRecord.Data.WriteBytes(buf)
|
||||
|
||||
clientHello, err := tlstypes.ParseClientHello(buf.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse client hello: %w", err)
|
||||
}
|
||||
|
||||
digest := clientHello.Digest()
|
||||
for i := 0; i < len(digest)-4; i++ {
|
||||
if digest[i] != 0 {
|
||||
return errBadDigest
|
||||
}
|
||||
}
|
||||
|
||||
timestamp := int64(binary.LittleEndian.Uint32(digest[len(digest)-4:]))
|
||||
createdAt := time.Unix(timestamp, 0)
|
||||
timeDiff := time.Since(createdAt)
|
||||
|
||||
if (timeDiff > TimeSkew || timeDiff < -TimeSkew) && timestamp > TimeFromBoot {
|
||||
return errBadTime
|
||||
}
|
||||
|
||||
if antireplay.Cache.HasTLS(clientHello.Random[:]) {
|
||||
stats.Stats.ReplayDetected()
|
||||
|
||||
return errors.New("replay attack is detected")
|
||||
}
|
||||
|
||||
antireplay.Cache.AddTLS(clientHello.Random[:])
|
||||
serverHello := tlstypes.NewServerHello(clientHello)
|
||||
serverHelloPacket := serverHello.WelcomePacket()
|
||||
|
||||
if _, err := conn.Write(serverHelloPacket); err != nil {
|
||||
return fmt.Errorf("cannot send welcome packet: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientProtocol) cloakHost(clientConn io.ReadWriteCloser) {
|
||||
stats.Stats.CloakedRequest()
|
||||
|
||||
addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort))
|
||||
|
||||
hostConn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cloak(clientConn, hostConn)
|
||||
}
|
||||
|
||||
func MakeClientProtocol() protocol.ClientProtocol {
|
||||
return &ClientProtocol{}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package faketls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/wrappers/rwc"
|
||||
)
|
||||
|
||||
const (
|
||||
cloakLastActivityTimeout = 5 * time.Second
|
||||
cloakMaxTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func cloak(one, another io.ReadWriteCloser) {
|
||||
defer func() {
|
||||
one.Close()
|
||||
another.Close()
|
||||
}()
|
||||
|
||||
channelPing := make(chan struct{}, 1)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
one = rwc.NewPing(ctx, one, channelPing)
|
||||
another = rwc.NewPing(ctx, another, channelPing)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
go cloakPipe(one, another, wg)
|
||||
|
||||
go cloakPipe(another, one, wg)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
lastActivityTimer := time.NewTimer(cloakLastActivityTimeout)
|
||||
defer lastActivityTimer.Stop()
|
||||
|
||||
maxTimer := time.NewTimer(cloakMaxTimeout)
|
||||
defer maxTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-channelPing:
|
||||
lastActivityTimer.Stop()
|
||||
lastActivityTimer = time.NewTimer(cloakLastActivityTimeout)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-lastActivityTimer.C:
|
||||
cancel()
|
||||
|
||||
return
|
||||
case <-maxTimer.C:
|
||||
cancel()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func cloakPipe(one io.Writer, another io.Reader, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
io.Copy(one, another) // nolint: errcheck
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package faketls
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TimeSkew = 5 * time.Second
|
||||
TimeFromBoot = 24 * 60 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
errBadDigest = errors.New("bad digest")
|
||||
errBadTime = errors.New("bad time")
|
||||
|
||||
faketlsStartBytes = [...]byte{
|
||||
0x16,
|
||||
0x03,
|
||||
0x01,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0xfc,
|
||||
0x03,
|
||||
0x03,
|
||||
}
|
||||
)
|
||||
@@ -1,32 +1,55 @@
|
||||
module github.com/9seconds/mtg
|
||||
module github.com/9seconds/mtg/v2
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/fastcache v1.6.0
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15
|
||||
github.com/beevik/ntp v0.3.0
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
github.com/alecthomas/kong v0.2.19
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gotd/td v0.34.0
|
||||
github.com/jarcoal/httpmock v1.0.8
|
||||
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e
|
||||
github.com/mccutchen/go-httpbin v1.1.1
|
||||
github.com/panjf2000/ants/v2 v2.4.6
|
||||
github.com/pelletier/go-toml v1.9.4
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/common v0.30.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rs/zerolog v1.26.0
|
||||
github.com/smira/go-statsd v1.3.2
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.19.0
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
)
|
||||
|
||||
require github.com/txthinking/socks5 v0.0.0-20211121111206-e03c1217a50b
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gotd/ige v0.1.5 // indirect
|
||||
github.com/gotd/xor v0.1.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
|
||||
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.16.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
@@ -31,28 +31,33 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o=
|
||||
github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/alecthomas/kong v0.2.19 h1:qBDfByO5XgWUXyNB4D6OOhGh5Z1eNOwWayDPQJFNWdc=
|
||||
github.com/alecthomas/kong v0.2.19/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
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/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
|
||||
github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc=
|
||||
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
|
||||
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
@@ -62,6 +67,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -78,7 +86,9 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -108,9 +118,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
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.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/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=
|
||||
@@ -136,9 +143,22 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gotd/getdoc v0.6.2/go.mod h1:6Ul8cyaq+Aw0gVyuU+TKZPCmNjdlylzSIWVxjzI5t/U=
|
||||
github.com/gotd/ige v0.1.5 h1:qITql4hZpqPM/DSeO5IVlxzXJxrs4ZZiKRwPif2K76U=
|
||||
github.com/gotd/ige v0.1.5/go.mod h1:LbvqjUuGELVuHcKjfJZ2NPNzegICQU4eqrPAkXy0nTs=
|
||||
github.com/gotd/neo v0.1.2 h1:hayFfKNSH5RP/L+KKKnsKMdDNCdhSfzVuo5TF4wvpa8=
|
||||
github.com/gotd/neo v0.1.2/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||
github.com/gotd/td v0.34.0 h1:BLIdpv2sxbCuWI2fyXCECRQuLJdnYk3VMZDRKNog/E8=
|
||||
github.com/gotd/td v0.34.0/go.mod h1:4s/7cuEscdvZIecM/pVi5L3nTK+djSoVLAhgfk40OE4=
|
||||
github.com/gotd/tl v0.4.0/go.mod h1:CMIcjPWFS4qxxJ+1Ce7U/ilbtPrkoVo/t8uhN5Y/D7c=
|
||||
github.com/gotd/xor v0.1.0/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
|
||||
github.com/gotd/xor v0.1.1 h1:LSPEeuf7noTo4fi4PrEsAaWXOSwjsY2e+IINPiR+c7s=
|
||||
github.com/gotd/xor v0.1.1/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
|
||||
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -147,21 +167,39 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/k0kubun/pp v2.4.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
|
||||
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e h1:1wAVuGu1c+lsdaOPQN+9xoP9+gaIMJV6H0ehGc+K5iA=
|
||||
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e/go.mod h1:2OfLA+0esiUJpwMjrH39pEk79cb8MvGTBS9YlZpejJ4=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
|
||||
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/panjf2000/ants/v2 v2.4.6 h1:drmj9mcygn2gawZ155dRbo+NfXEfAssjZNU1qoIb4gQ=
|
||||
github.com/panjf2000/ants/v2 v2.4.6/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -181,15 +219,23 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
|
||||
github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.2/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
|
||||
github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
|
||||
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
@@ -197,38 +243,50 @@ github.com/smira/go-statsd v1.3.2 h1:1EeuzxNZ/TD9apbTOFSM9nulqfcsQFmT4u1A2DREabI
|
||||
github.com/smira/go-statsd v1.3.2/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.1.5-0.20170809224252-890a5c3458b4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
||||
github.com/txthinking/socks5 v0.0.0-20211121111206-e03c1217a50b h1:6J/38A0Xmdnjacfie0Udams7OP/GdoExyTipKwuQWjY=
|
||||
github.com/txthinking/socks5 v0.0.0-20211121111206-e03c1217a50b/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg=
|
||||
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A=
|
||||
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4=
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg=
|
||||
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
|
||||
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -260,6 +318,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -289,8 +350,10 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -305,12 +368,14 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -319,8 +384,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -341,13 +406,13 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -373,7 +438,8 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@@ -398,9 +464,10 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
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=
|
||||
@@ -483,17 +550,17 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
@@ -506,6 +573,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/conntypes"
|
||||
"github.com/9seconds/mtg/mtproto"
|
||||
"github.com/9seconds/mtg/mtproto/rpc"
|
||||
"github.com/9seconds/mtg/protocol"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const connectionTTL = time.Hour
|
||||
|
||||
type connection struct {
|
||||
conn conntypes.PacketReadWriteCloser
|
||||
proxyConns map[string]*ProxyConn
|
||||
closeOnce sync.Once
|
||||
proxyConnsMutex sync.RWMutex
|
||||
id int
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
channelDone chan struct{}
|
||||
channelWrite chan conntypes.Packet
|
||||
channelRead chan *rpc.ProxyResponse
|
||||
channelConnAttach chan *ProxyConn
|
||||
channelConnDetach chan conntypes.ConnID
|
||||
}
|
||||
|
||||
func (c *connection) run() { // nolint: cyclop
|
||||
defer c.Close()
|
||||
|
||||
ttl := time.NewTimer(connectionTTL)
|
||||
defer ttl.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.channelDone:
|
||||
for _, v := range c.proxyConns {
|
||||
v.Close()
|
||||
}
|
||||
|
||||
return
|
||||
case <-ttl.C:
|
||||
c.logger.Debugw("Closing connection by TTL")
|
||||
c.Close()
|
||||
case resp := <-c.channelRead:
|
||||
if channel, ok := c.proxyConns[string(resp.ConnID[:])]; ok {
|
||||
if resp.Type == rpc.ProxyResponseTypeCloseExt {
|
||||
channel.Close()
|
||||
} else {
|
||||
channel.put(resp)
|
||||
}
|
||||
}
|
||||
case packet := <-c.channelWrite:
|
||||
if err := c.conn.Write(packet); err != nil {
|
||||
c.logger.Debugw("Cannot write packet", "error", err)
|
||||
c.Close()
|
||||
}
|
||||
case conn := <-c.channelConnAttach:
|
||||
c.proxyConnsMutex.Lock()
|
||||
c.proxyConns[string(conn.req.ConnID[:])] = conn
|
||||
c.proxyConnsMutex.Unlock()
|
||||
conn.channelWrite = c.channelWrite
|
||||
case connID := <-c.channelConnDetach:
|
||||
if conn, ok := c.proxyConns[string(connID[:])]; ok {
|
||||
c.proxyConnsMutex.Lock()
|
||||
delete(c.proxyConns, string(connID[:]))
|
||||
c.proxyConnsMutex.Unlock()
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) readLoop() {
|
||||
for {
|
||||
packet, err := c.conn.Read()
|
||||
if err != nil {
|
||||
c.logger.Debugw("Cannot read packet", "error", err)
|
||||
c.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response, err := rpc.ParseProxyResponse(packet)
|
||||
if err != nil {
|
||||
c.logger.Debugw("Failed response", "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.channelDone:
|
||||
return
|
||||
case c.channelRead <- response:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) Close() {
|
||||
c.closeOnce.Do(func() {
|
||||
c.logger.Debugw("Closing connection")
|
||||
|
||||
close(c.channelDone)
|
||||
c.conn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *connection) Done() bool {
|
||||
select {
|
||||
case <-c.channelDone:
|
||||
return true
|
||||
default:
|
||||
return c.Len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) Len() int {
|
||||
c.proxyConnsMutex.RLock()
|
||||
defer c.proxyConnsMutex.RUnlock()
|
||||
|
||||
return len(c.proxyConns)
|
||||
}
|
||||
|
||||
func (c *connection) Attach(conn *ProxyConn) error {
|
||||
select {
|
||||
case <-c.channelDone:
|
||||
return ErrClosed
|
||||
case c.channelConnAttach <- conn:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) Detach(connID conntypes.ConnID) {
|
||||
select {
|
||||
case <-c.channelDone:
|
||||
case c.channelConnDetach <- connID:
|
||||
}
|
||||
}
|
||||
|
||||
func newConnection(req *protocol.TelegramRequest) (*connection, error) {
|
||||
conn, err := mtproto.TelegramProtocol(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create a new connection: %w", err)
|
||||
}
|
||||
|
||||
id := rand.Int() // nolint: gosec
|
||||
rv := &connection{
|
||||
conn: conn,
|
||||
id: id,
|
||||
logger: zap.S().Named("hub-connection").With("id", id,
|
||||
"dc", req.ClientProtocol.DC(),
|
||||
"protocol", req.ClientProtocol.ConnectionProtocol()),
|
||||
proxyConns: make(map[string]*ProxyConn),
|
||||
|
||||
channelRead: make(chan *rpc.ProxyResponse, 1),
|
||||
channelDone: make(chan struct{}),
|
||||
channelWrite: make(chan conntypes.Packet),
|
||||
channelConnAttach: make(chan *ProxyConn),
|
||||
channelConnDetach: make(chan conntypes.ConnID),
|
||||
}
|
||||
|
||||
go rv.readLoop()
|
||||
|
||||
go rv.run()
|
||||
|
||||
return rv, nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/9seconds/mtg/config"
|
||||
)
|
||||
|
||||
type connectionList struct {
|
||||
connections []*connection
|
||||
}
|
||||
|
||||
func (c *connectionList) get(conn *ProxyConn) (*connection, error) {
|
||||
if len(c.connections) > 0 && c.connections[0].Len() < config.C.MultiplexPerConnection {
|
||||
if err := c.connections[0].Attach(conn); err == nil {
|
||||
return c.connections[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
newConn, err := newConnection(conn.req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot allocate a new connection: %w", err)
|
||||
}
|
||||
|
||||
if err = newConn.Attach(conn); err != nil {
|
||||
newConn.Close()
|
||||
|
||||
return nil, fmt.Errorf("cannot attach to the newly created connection: %w", err)
|
||||
}
|
||||
|
||||
c.connections = append(c.connections, newConn)
|
||||
lastIndex := len(c.connections) - 1
|
||||
c.connections[0], c.connections[lastIndex] = c.connections[lastIndex], c.connections[0]
|
||||
|
||||
return newConn, nil
|
||||
}
|
||||
|
||||
func (c *connectionList) gc() {
|
||||
prevLen := len(c.connections)
|
||||
if prevLen == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(c.connections) - 1; i >= 0; i-- {
|
||||
lastIndex := len(c.connections) - 1
|
||||
|
||||
if c.connections[i].Done() {
|
||||
c.connections[i].Close()
|
||||
|
||||
if len(c.connections)-1 == i {
|
||||
c.connections = c.connections[:lastIndex]
|
||||
} else {
|
||||
c.connections[i], c.connections[lastIndex] = c.connections[lastIndex], c.connections[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prevLen != len(c.connections) {
|
||||
c.sort()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connectionList) sort() {
|
||||
if len(c.connections) > 1 {
|
||||
sort.Slice(c.connections, func(i, j int) bool {
|
||||
return c.connections[i].Len() < c.connections[j].Len()
|
||||
})
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/protocol"
|
||||
)
|
||||
|
||||
type hub struct {
|
||||
muxes map[int32]*mux
|
||||
mutex sync.RWMutex
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (h *hub) Register(req *protocol.TelegramRequest) (*ProxyConn, error) {
|
||||
return h.getMux(req).Get(req)
|
||||
}
|
||||
|
||||
func (h *hub) getMux(req *protocol.TelegramRequest) *mux {
|
||||
key := 32767 + int32(req.ClientProtocol.DC()) + 100000*int32(req.ClientProtocol.ConnectionProtocol())
|
||||
|
||||
h.mutex.RLock()
|
||||
m, ok := h.muxes[key]
|
||||
h.mutex.RUnlock()
|
||||
|
||||
if !ok {
|
||||
h.mutex.Lock()
|
||||
m, ok = h.muxes[key]
|
||||
|
||||
if !ok {
|
||||
m = newMux(h.ctx)
|
||||
h.muxes[key] = m
|
||||
}
|
||||
|
||||
h.mutex.Unlock()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTimeout = errors.New("timeout")
|
||||
ErrClosed = errors.New("context is closed")
|
||||
|
||||
Hub Interface
|
||||
initOnce sync.Once
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) {
|
||||
initOnce.Do(func() {
|
||||
Hub = &hub{
|
||||
muxes: make(map[int32]*mux),
|
||||
ctx: ctx,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package hub
|
||||
|
||||
import "github.com/9seconds/mtg/protocol"
|
||||
|
||||
type Interface interface {
|
||||
Register(*protocol.TelegramRequest) (*ProxyConn, error)
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/conntypes"
|
||||
"github.com/9seconds/mtg/protocol"
|
||||
)
|
||||
|
||||
const muxGCEvery = time.Minute
|
||||
|
||||
type muxNewRequest struct {
|
||||
req *protocol.TelegramRequest
|
||||
resp chan<- muxNewResponse
|
||||
}
|
||||
|
||||
type muxNewResponse struct {
|
||||
conn *ProxyConn
|
||||
err error
|
||||
}
|
||||
|
||||
type mux struct {
|
||||
connections connectionList
|
||||
clients map[string]*connection
|
||||
ctx context.Context
|
||||
channelClosed chan conntypes.ConnID
|
||||
channelNew chan muxNewRequest
|
||||
}
|
||||
|
||||
func (m *mux) run() {
|
||||
gcTicker := time.NewTicker(muxGCEvery)
|
||||
defer gcTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
for _, v := range m.clients {
|
||||
v.Close()
|
||||
}
|
||||
|
||||
return
|
||||
case <-gcTicker.C:
|
||||
m.connections.gc()
|
||||
case req := <-m.channelNew:
|
||||
m.connections.gc()
|
||||
proxyConn := newProxyConn(req.req, m.channelClosed)
|
||||
conn, err := m.connections.get(proxyConn)
|
||||
|
||||
if err == nil {
|
||||
m.clients[string(req.req.ConnID[:])] = conn
|
||||
}
|
||||
|
||||
req.resp <- muxNewResponse{
|
||||
conn: proxyConn,
|
||||
err: err,
|
||||
}
|
||||
close(req.resp)
|
||||
case connID := <-m.channelClosed:
|
||||
if conn, ok := m.clients[string(connID[:])]; ok {
|
||||
conn.Detach(connID)
|
||||
delete(m.clients, string(connID[:]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mux) Get(req *protocol.TelegramRequest) (*ProxyConn, error) {
|
||||
resp := make(chan muxNewResponse)
|
||||
m.channelNew <- muxNewRequest{
|
||||
req: req,
|
||||
resp: resp,
|
||||
}
|
||||
|
||||
rv := <-resp
|
||||
|
||||
return rv.conn, rv.err
|
||||
}
|
||||
|
||||
func newMux(ctx context.Context) *mux {
|
||||
m := &mux{
|
||||
ctx: ctx,
|
||||
clients: make(map[string]*connection),
|
||||
channelClosed: make(chan conntypes.ConnID, 1),
|
||||
channelNew: make(chan muxNewRequest),
|
||||
}
|
||||
go m.run()
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/conntypes"
|
||||
"github.com/9seconds/mtg/mtproto/rpc"
|
||||
"github.com/9seconds/mtg/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyConnWriteTimeout = 2 * time.Minute
|
||||
proxyConnReadTimeout = 2 * time.Minute
|
||||
|
||||
proxyConnBackpressureAfter = 10
|
||||
)
|
||||
|
||||
type ProxyConn struct {
|
||||
closeOnce sync.Once
|
||||
req *protocol.TelegramRequest
|
||||
channelResponse chan *rpc.ProxyResponse
|
||||
channelClosed chan<- conntypes.ConnID
|
||||
channelWrite chan<- conntypes.Packet
|
||||
channelDone chan struct{}
|
||||
}
|
||||
|
||||
func (p *ProxyConn) Read() (*rpc.ProxyResponse, error) {
|
||||
timer := time.NewTimer(proxyConnReadTimeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return nil, ErrTimeout
|
||||
case <-p.channelDone:
|
||||
return nil, ErrClosed
|
||||
case packet := <-p.channelResponse:
|
||||
return packet, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxyConn) Write(packet conntypes.Packet) error {
|
||||
timer := time.NewTimer(proxyConnWriteTimeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return ErrTimeout
|
||||
case <-p.channelDone:
|
||||
return ErrClosed
|
||||
case p.channelWrite <- packet:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxyConn) put(response *rpc.ProxyResponse) {
|
||||
select {
|
||||
case <-p.channelDone:
|
||||
case p.channelResponse <- response:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxyConn) Close() {
|
||||
p.closeOnce.Do(func() {
|
||||
close(p.channelDone)
|
||||
go func() {
|
||||
p.channelClosed <- p.req.ConnID
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func newProxyConn(req *protocol.TelegramRequest, channelClosed chan<- conntypes.ConnID) *ProxyConn {
|
||||
return &ProxyConn{
|
||||
channelResponse: make(chan *rpc.ProxyResponse, proxyConnBackpressureAfter),
|
||||
channelDone: make(chan struct{}),
|
||||
channelClosed: channelClosed,
|
||||
req: req,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/9seconds/mtg/v2/essentials"
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type accessResponse struct {
|
||||
IPv4 *accessResponseURLs `json:"ipv4,omitempty"`
|
||||
IPv6 *accessResponseURLs `json:"ipv6,omitempty"`
|
||||
Secret struct {
|
||||
Hex string `json:"hex"`
|
||||
Base64 string `json:"base64"`
|
||||
} `json:"secret"`
|
||||
}
|
||||
|
||||
type accessResponseURLs struct {
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint `json:"port"`
|
||||
TgURL string `json:"tg_url"` // nolint: tagliatelle
|
||||
TgQrCode string `json:"tg_qrcode"` // nolint: tagliatelle
|
||||
TmeURL string `json:"tme_url"` // nolint: tagliatelle
|
||||
TmeQrCode string `json:"tme_qrcode"` // nolint: tagliatelle
|
||||
}
|
||||
|
||||
type Access struct {
|
||||
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
|
||||
PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` // nolint: lll
|
||||
PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` // nolint: lll
|
||||
Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` // nolint: lll
|
||||
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
|
||||
}
|
||||
|
||||
func (a *Access) Run(cli *CLI, version string) error {
|
||||
conf, err := utils.ReadConfig(a.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init config: %w", err)
|
||||
}
|
||||
|
||||
resp := &accessResponse{}
|
||||
resp.Secret.Base64 = conf.Secret.Base64()
|
||||
resp.Secret.Hex = conf.Secret.Hex()
|
||||
|
||||
ntw, err := makeNetwork(conf, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init network: %w", err)
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2) // nolint: gomnd
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ip := a.PublicIPv4
|
||||
if ip == nil {
|
||||
ip = a.getIP(ntw, "tcp4")
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
|
||||
resp.IPv4 = a.makeURLs(conf, ip)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ip := a.PublicIPv6
|
||||
if ip == nil {
|
||||
ip = a.getIP(ntw, "tcp6")
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
ip = ip.To16()
|
||||
}
|
||||
|
||||
resp.IPv6 = a.makeURLs(conf, ip)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(resp); err != nil {
|
||||
return fmt.Errorf("cannot dump access json: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
|
||||
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
|
||||
return ntw.DialContext(ctx, protocol, address) // nolint: wrapcheck
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "text/plain")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
io.Copy(io.Discard, resp.Body) // nolint: errcheck
|
||||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return net.ParseIP(strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
portNo := a.Port
|
||||
if portNo == 0 {
|
||||
portNo = conf.BindTo.Port
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("server", ip.String())
|
||||
values.Set("port", strconv.Itoa(int(portNo)))
|
||||
|
||||
if a.Hex {
|
||||
values.Set("secret", conf.Secret.Hex())
|
||||
} else {
|
||||
values.Set("secret", conf.Secret.Base64())
|
||||
}
|
||||
|
||||
urlQuery := values.Encode()
|
||||
|
||||
rv := &accessResponseURLs{
|
||||
IP: ip,
|
||||
Port: portNo,
|
||||
TgURL: (&url.URL{
|
||||
Scheme: "tg",
|
||||
Host: "proxy",
|
||||
RawQuery: urlQuery,
|
||||
}).String(),
|
||||
TmeURL: (&url.URL{
|
||||
Scheme: "https",
|
||||
Host: "t.me",
|
||||
Path: "proxy",
|
||||
RawQuery: urlQuery,
|
||||
}).String(),
|
||||
}
|
||||
rv.TgQrCode = utils.MakeQRCodeURL(rv.TgURL)
|
||||
rv.TmeQrCode = utils.MakeQRCodeURL(rv.TmeURL)
|
||||
|
||||
return rv
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cli
|
||||
|
||||
import "github.com/alecthomas/kong"
|
||||
|
||||
type CLI struct {
|
||||
GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"`
|
||||
Access Access `kong:"cmd,help='Print access information.'"`
|
||||
Run Run `kong:"cmd,help='Run proxy.'"`
|
||||
SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"`
|
||||
Version kong.VersionFlag `kong:"help='Print version.',short='v'"`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type GenerateSecret struct {
|
||||
HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"`
|
||||
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
|
||||
}
|
||||
|
||||
func (g *GenerateSecret) Run(cli *CLI, _ string) error {
|
||||
secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
|
||||
|
||||
if g.Hex {
|
||||
fmt.Println(secret.Hex()) // nolint: forbidigo
|
||||
} else {
|
||||
fmt.Println(secret.Base64()) // nolint: forbidigo
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
)
|
||||
|
||||
type Run struct {
|
||||
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
|
||||
}
|
||||
|
||||
func (r *Run) Run(cli *CLI, version string) error {
|
||||
conf, err := utils.ReadConfig(r.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init config: %w", err)
|
||||
}
|
||||
|
||||
return runProxy(conf, version)
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/9seconds/mtg/v2/antireplay"
|
||||
"github.com/9seconds/mtg/v2/events"
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/9seconds/mtg/v2/internal/utils"
|
||||
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||
"github.com/9seconds/mtg/v2/logger"
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
"github.com/9seconds/mtg/v2/network"
|
||||
"github.com/9seconds/mtg/v2/stats"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func makeLogger(conf *config.Config) mtglib.Logger {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFieldName = "level"
|
||||
|
||||
if conf.Debug.Get(false) {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
} else {
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
}
|
||||
|
||||
baseLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
return logger.NewZeroLogger(baseLogger)
|
||||
}
|
||||
|
||||
func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
|
||||
tcpTimeout := conf.Network.Timeout.TCP.Get(network.DefaultTimeout)
|
||||
httpTimeout := conf.Network.Timeout.HTTP.Get(network.DefaultHTTPTimeout)
|
||||
dohIP := conf.Network.DOHIP.Get(net.ParseIP(network.DefaultDOHHostname)).String()
|
||||
userAgent := "mtg/" + version
|
||||
|
||||
baseDialer, err := network.NewDefaultDialer(tcpTimeout, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build a default dialer: %w", err)
|
||||
}
|
||||
|
||||
if len(conf.Network.Proxies) == 0 {
|
||||
return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
|
||||
|
||||
for _, v := range conf.Network.Proxies {
|
||||
if value := v.Get(nil); value != nil {
|
||||
proxyURLs = append(proxyURLs, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(proxyURLs) == 1 {
|
||||
socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
|
||||
}
|
||||
|
||||
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
|
||||
}
|
||||
|
||||
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
|
||||
if !conf.Defense.AntiReplay.Enabled.Get(false) {
|
||||
return antireplay.NewNoop()
|
||||
}
|
||||
|
||||
return antireplay.NewStableBloomFilter(
|
||||
conf.Defense.AntiReplay.MaxSize.Get(antireplay.DefaultStableBloomFilterMaxSize),
|
||||
conf.Defense.AntiReplay.ErrorRate.Get(antireplay.DefaultStableBloomFilterErrorRate),
|
||||
)
|
||||
}
|
||||
|
||||
func makeIPBlocklist(conf config.ListConfig, logger mtglib.Logger, ntw mtglib.Network) (mtglib.IPBlocklist, error) {
|
||||
if !conf.Enabled.Get(false) {
|
||||
return ipblocklist.NewNoop(), nil
|
||||
}
|
||||
|
||||
remoteURLs := []string{}
|
||||
localFiles := []string{}
|
||||
|
||||
for _, v := range conf.URLs {
|
||||
if v.IsRemote() {
|
||||
remoteURLs = append(remoteURLs, v.String())
|
||||
} else {
|
||||
localFiles = append(localFiles, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
firehol, err := ipblocklist.NewFirehol(logger.Named("ipblockist"),
|
||||
ntw,
|
||||
conf.DownloadConcurrency.Get(1),
|
||||
remoteURLs,
|
||||
localFiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("incorrect parameters for firehol: %w", err)
|
||||
}
|
||||
|
||||
return firehol, nil
|
||||
}
|
||||
|
||||
func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) {
|
||||
factories := make([]events.ObserverFactory, 0, 2) // nolint: gomnd
|
||||
|
||||
if conf.Stats.StatsD.Enabled.Get(false) {
|
||||
statsdFactory, err := stats.NewStatsd(
|
||||
conf.Stats.StatsD.Address.Get(""),
|
||||
logger.Named("statsd"),
|
||||
conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix),
|
||||
conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build statsd observer: %w", err)
|
||||
}
|
||||
|
||||
factories = append(factories, statsdFactory.Make)
|
||||
}
|
||||
|
||||
if conf.Stats.Prometheus.Enabled.Get(false) {
|
||||
prometheus := stats.NewPrometheus(
|
||||
conf.Stats.Prometheus.MetricPrefix.Get(stats.DefaultMetricPrefix),
|
||||
conf.Stats.Prometheus.HTTPPath.Get("/"),
|
||||
)
|
||||
|
||||
listener, err := net.Listen("tcp", conf.Stats.Prometheus.BindTo.Get(""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot start a listener for prometheus: %w", err)
|
||||
}
|
||||
|
||||
go prometheus.Serve(listener) // nolint: errcheck
|
||||
|
||||
factories = append(factories, prometheus.Make)
|
||||
}
|
||||
|
||||
if len(factories) > 0 {
|
||||
return events.NewEventStream(factories), nil
|
||||
}
|
||||
|
||||
return events.NewNoopStream(), nil
|
||||
}
|
||||
|
||||
func runProxy(conf *config.Config, version string) error { // nolint: funlen
|
||||
logger := makeLogger(conf)
|
||||
|
||||
logger.BindJSON("configuration", conf.String()).Debug("configuration")
|
||||
|
||||
ntw, err := makeNetwork(conf, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build network: %w", err)
|
||||
}
|
||||
|
||||
blocklist, err := makeIPBlocklist(conf.Defense.Blocklist, logger, ntw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build ip blocklist: %w", err)
|
||||
}
|
||||
|
||||
var whitelist mtglib.IPBlocklist
|
||||
|
||||
if conf.Defense.Allowlist.Enabled.Get(false) {
|
||||
whlist, err := makeIPBlocklist(conf.Defense.Allowlist, logger, ntw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build ip blocklist: %w", err)
|
||||
}
|
||||
|
||||
whitelist = whlist
|
||||
}
|
||||
|
||||
eventStream, err := makeEventStream(conf, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build event stream: %w", err)
|
||||
}
|
||||
|
||||
opts := mtglib.ProxyOpts{
|
||||
Logger: logger,
|
||||
Network: ntw,
|
||||
AntiReplayCache: makeAntiReplayCache(conf),
|
||||
IPBlocklist: blocklist,
|
||||
IPWhitelist: whitelist,
|
||||
EventStream: eventStream,
|
||||
|
||||
Secret: conf.Secret,
|
||||
DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort),
|
||||
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
|
||||
|
||||
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
|
||||
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
|
||||
}
|
||||
|
||||
proxy, err := mtglib.NewProxy(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create a proxy: %w", err)
|
||||
}
|
||||
|
||||
listener, err := utils.NewListener(conf.BindTo.Get(""), 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start proxy: %w", err)
|
||||
}
|
||||
|
||||
ctx := utils.RootContext()
|
||||
|
||||
go proxy.Serve(listener) // nolint: errcheck
|
||||
|
||||
<-ctx.Done()
|
||||
listener.Close()
|
||||
proxy.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
)
|
||||
|
||||
type SimpleRun struct {
|
||||
BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
|
||||
Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`
|
||||
|
||||
Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` // nolint: lll
|
||||
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` // nolint: lll
|
||||
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` // nolint: lll
|
||||
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` // nolint: lll
|
||||
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` // nolint: lll
|
||||
DOHIP net.IP `kong:"name='doh-ip',short='n',default='9.9.9.9',help='IP address of DNS-over-HTTP to use.'"` // nolint: lll
|
||||
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` // nolint: lll
|
||||
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` // nolint: lll
|
||||
}
|
||||
|
||||
func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop
|
||||
conf := &config.Config{}
|
||||
|
||||
if err := conf.BindTo.Set(s.BindTo); err != nil {
|
||||
return fmt.Errorf("incorrect bind-to parameter: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Secret.Set(s.Secret); err != nil {
|
||||
return fmt.Errorf("incorrect secret: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Concurrency.Set(strconv.FormatUint(s.Concurrency, 10)); err != nil { // nolint: gomnd
|
||||
return fmt.Errorf("incorrect concurrency: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.PreferIP.Set(s.PreferIP); err != nil {
|
||||
return fmt.Errorf("incorrect prefer-ip: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.DomainFrontingPort.Set(strconv.FormatUint(s.DomainFrontingPort, 10)); err != nil { // nolint: gomnd
|
||||
return fmt.Errorf("incorrect domain-fronting-port: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.DOHIP.Set(s.DOHIP.String()); err != nil {
|
||||
return fmt.Errorf("incorrect doh-ip: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.Timeout.TCP.Set(s.Timeout.String()); err != nil {
|
||||
return fmt.Errorf("incorrect timeout: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.Timeout.HTTP.Set(s.Timeout.String()); err != nil {
|
||||
return fmt.Errorf("incorrect timeout: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Network.Timeout.Idle.Set(s.Timeout.String()); err != nil {
|
||||
return fmt.Errorf("incorrect timeout: %w", err)
|
||||
}
|
||||
|
||||
if err := conf.Defense.AntiReplay.MaxSize.Set(s.AntiReplayCacheSize); err != nil {
|
||||
return fmt.Errorf("incorrect antireplay-cache-size: %w", err)
|
||||
}
|
||||
|
||||
conf.Debug.Value = s.Debug
|
||||
conf.AllowFallbackOnUnknownDC.Value = true
|
||||
conf.Defense.AntiReplay.Enabled.Value = true
|
||||
|
||||
if err := conf.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid result configuration: %w", err)
|
||||
}
|
||||
|
||||
return runProxy(conf, version)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib"
|
||||
)
|
||||
|
||||
type Optional struct {
|
||||
Enabled TypeBool `json:"enabled"`
|
||||
}
|
||||
|
||||
type ListConfig struct {
|
||||
Optional
|
||||
|
||||
DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"`
|
||||
URLs []TypeBlocklistURI `json:"urls"`
|
||||
UpdateEach TypeDuration `json:"updateEach"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Debug TypeBool `json:"debug"`
|
||||
AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"`
|
||||
Secret mtglib.Secret `json:"secret"`
|
||||
BindTo TypeHostPort `json:"bindTo"`
|
||||
PreferIP TypePreferIP `json:"preferIp"`
|
||||
DomainFrontingPort TypePort `json:"domainFrontingPort"`
|
||||
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
|
||||
Concurrency TypeConcurrency `json:"concurrency"`
|
||||
Defense struct {
|
||||
AntiReplay struct {
|
||||
Optional
|
||||
|
||||
MaxSize TypeBytes `json:"maxSize"`
|
||||
ErrorRate TypeErrorRate `json:"errorRate"`
|
||||
} `json:"antiReplay"`
|
||||
Blocklist ListConfig `json:"blocklist"`
|
||||
Allowlist ListConfig `json:"allowlist"`
|
||||
} `json:"defense"`
|
||||
Network struct {
|
||||
Timeout struct {
|
||||
TCP TypeDuration `json:"tcp"`
|
||||
HTTP TypeDuration `json:"http"`
|
||||
Idle TypeDuration `json:"idle"`
|
||||
} `json:"timeout"`
|
||||
DOHIP TypeIP `json:"dohIp"`
|
||||
Proxies []TypeProxyURL `json:"proxies"`
|
||||
} `json:"network"`
|
||||
Stats struct {
|
||||
StatsD struct {
|
||||
Optional
|
||||
|
||||
Address TypeHostPort `json:"address"`
|
||||
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||
TagFormat TypeStatsdTagFormat `json:"tagFormat"`
|
||||
} `json:"statsd"`
|
||||
Prometheus struct {
|
||||
Optional
|
||||
|
||||
BindTo TypeHostPort `json:"bindTo"`
|
||||
HTTPPath TypeHTTPPath `json:"httpPath"`
|
||||
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||
} `json:"prometheus"`
|
||||
} `json:"stats"`
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if !c.Secret.Valid() {
|
||||
return fmt.Errorf("invalid secret %s", c.Secret.String())
|
||||
}
|
||||
|
||||
if c.BindTo.Get("") == "" {
|
||||
return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) String() string {
|
||||
buf := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buf)
|
||||
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
if err := encoder.Encode(c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ConfigTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) ReadConfig(filename string) []byte {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", filename))
|
||||
suite.NoError(err)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseEmpty() {
|
||||
_, err := config.Parse([]byte{})
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseBrokenToml() {
|
||||
_, err := config.Parse(suite.ReadConfig("broken.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseOnlySecret() {
|
||||
_, err := config.Parse(suite.ReadConfig("only_secret.toml"))
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestParseMinimalConfig() {
|
||||
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||
suite.NoError(err)
|
||||
suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64())
|
||||
suite.Equal("0.0.0.0:3128", conf.BindTo.String())
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestString() {
|
||||
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(conf.String())
|
||||
}
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ConfigTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
type tomlConfig struct {
|
||||
Debug bool `toml:"debug" json:"debug,omitempty"`
|
||||
AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"`
|
||||
Secret string `toml:"secret" json:"secret"`
|
||||
BindTo string `toml:"bind-to" json:"bindTo"`
|
||||
PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"`
|
||||
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`
|
||||
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
|
||||
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
|
||||
Defense struct {
|
||||
AntiReplay struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
MaxSize string `toml:"max-size" json:"maxSize,omitempty"`
|
||||
ErrorRate float64 `toml:"error-rate" json:"errorRate,omitempty"`
|
||||
} `toml:"anti-replay" json:"antiReplay,omitempty"`
|
||||
Blocklist struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"`
|
||||
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
|
||||
} `toml:"blocklist" json:"blocklist,omitempty"`
|
||||
Allowlist struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"`
|
||||
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
|
||||
} `toml:"allowlist" json:"allowlist,omitempty"`
|
||||
} `toml:"defense" json:"defense,omitempty"`
|
||||
Network struct {
|
||||
Timeout struct {
|
||||
TCP string `toml:"tcp" json:"tcp,omitempty"`
|
||||
HTTP string `toml:"http" json:"http,omitempty"`
|
||||
Idle string `toml:"idle" json:"idle,omitempty"`
|
||||
} `toml:"timeout" json:"timeout,omitempty"`
|
||||
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
|
||||
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
|
||||
} `toml:"network" json:"network,omitempty"`
|
||||
Stats struct {
|
||||
StatsD struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
Address string `toml:"address" json:"address,omitempty"`
|
||||
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||
TagFormat string `toml:"tag-format" json:"tagFormat,omitempty"`
|
||||
} `toml:"statsd" json:"statsd,omitempty"`
|
||||
Prometheus struct {
|
||||
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||
BindTo string `toml:"bind-to" json:"bindTo,omitempty"`
|
||||
HTTPPath string `toml:"http-path" json:"httpPath,omitempty"`
|
||||
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||
} `toml:"prometheus" json:"prometheus,omitempty"`
|
||||
} `toml:"stats" json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
func Parse(rawData []byte) (*Config, error) {
|
||||
tomlConf := &tomlConfig{}
|
||||
jsonBuf := &bytes.Buffer{}
|
||||
conf := &Config{}
|
||||
|
||||
jsonEncoder := json.NewEncoder(jsonBuf)
|
||||
jsonEncoder.SetEscapeHTML(false)
|
||||
jsonEncoder.SetIndent("", "")
|
||||
|
||||
if err := toml.Unmarshal(rawData, tomlConf); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse toml config: %w", err)
|
||||
}
|
||||
|
||||
if err := jsonEncoder.Encode(tomlConf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse a config: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
s = sdfsdfds
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||
bind-to = "0.0.0.0:3128"
|
||||
+1
@@ -0,0 +1 @@
|
||||
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||
@@ -0,0 +1,77 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type TypeBlocklistURI struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeBlocklistURI) Set(value string) error {
|
||||
if stat, err := os.Stat(value); err == nil || os.IsExist(err) {
|
||||
switch {
|
||||
case stat.IsDir():
|
||||
return fmt.Errorf("value is correct filepath but directory")
|
||||
case stat.Mode().Perm()&0o400 == 0:
|
||||
return fmt.Errorf("value is correct filepath but not readable")
|
||||
}
|
||||
|
||||
value, err = filepath.Abs(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"value is correct filepath but cannot resolve absolute (%s): %w",
|
||||
value, err)
|
||||
}
|
||||
|
||||
t.Value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect url (%s): %w", value, err)
|
||||
}
|
||||
|
||||
switch parsedURL.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value)
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("incorrect url %s", value)
|
||||
}
|
||||
|
||||
t.Value = parsedURL.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) IsRemote() bool {
|
||||
return !filepath.IsAbs(t.Value)
|
||||
}
|
||||
|
||||
func (t *TypeBlocklistURI) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeBlocklistURI) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeBlocklistURITestStruct struct {
|
||||
Value config.TypeBlocklistURI `json:"value"`
|
||||
}
|
||||
|
||||
type TypeBlocklistURITestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
directory string
|
||||
absDirectory string
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) SetupSuite() {
|
||||
dir, _ := os.Getwd()
|
||||
absDir, _ := filepath.Abs(dir)
|
||||
|
||||
suite.directory = dir
|
||||
suite.absDirectory = absDir
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"gopher://lalala",
|
||||
"https:///paths",
|
||||
"h:/=",
|
||||
filepath.Join(suite.directory, "___"),
|
||||
filepath.Join(suite.absDirectory, "___"),
|
||||
suite.directory,
|
||||
suite.absDirectory,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
"http://lalala",
|
||||
"https://lalala",
|
||||
"https://lalala/path",
|
||||
filepath.Join(suite.directory, "config.go"),
|
||||
filepath.Join(suite.absDirectory, "config.go"),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeBlocklistURITestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.EqualValues(t, value, testStruct.Value.Get(""))
|
||||
|
||||
if strings.HasPrefix(value, "http") {
|
||||
assert.True(t, testStruct.Value.IsRemote())
|
||||
} else {
|
||||
assert.False(t, testStruct.Value.IsRemote())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestMarshalOk() {
|
||||
testStruct := &typeBlocklistURITestStruct{
|
||||
Value: config.TypeBlocklistURI{
|
||||
Value: "http://some.url/with/path",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "http://some.url/with/path"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeBlocklistURITestSuite) TestGet() {
|
||||
value := config.TypeBlocklistURI{}
|
||||
suite.Equal("/path", value.Get("/path"))
|
||||
|
||||
suite.NoError(value.Set("http://lalala.ru"))
|
||||
suite.Equal("http://lalala.ru", value.Get("/path"))
|
||||
suite.Equal("http://lalala.ru", value.Get(""))
|
||||
}
|
||||
|
||||
func TestTypeBlocklistURI(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBlocklistURITestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeBool struct {
|
||||
Value bool
|
||||
}
|
||||
|
||||
func (t *TypeBool) Set(data string) error {
|
||||
parsed, err := strconv.ParseBool(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect bool value: %s", data)
|
||||
}
|
||||
|
||||
t.Value = parsed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeBool) Get(defaultValue bool) bool {
|
||||
return t.Value || defaultValue
|
||||
}
|
||||
|
||||
func (t *TypeBool) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeBool) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeBool) String() string {
|
||||
return strconv.FormatBool(t.Value)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeBoolTestStruct struct {
|
||||
Value config.TypeBool `json:"value"`
|
||||
}
|
||||
|
||||
type TypeBoolTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestUnmarshalFail() {
|
||||
testData := []interface{}{
|
||||
"",
|
||||
"np",
|
||||
"нет",
|
||||
int(10),
|
||||
[]int{},
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]interface{}{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(fmt.Sprintf("%v", v), func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestUnmarshalOk() {
|
||||
testData := []bool{
|
||||
true,
|
||||
false,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]bool{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(strconv.FormatBool(v), func(t *testing.T) {
|
||||
testStruct := &typeBoolTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
|
||||
if value {
|
||||
assert.True(t, testStruct.Value.Value)
|
||||
} else {
|
||||
assert.False(t, testStruct.Value.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestMarshalOk() {
|
||||
for _, v := range []bool{true, false} {
|
||||
value := v
|
||||
|
||||
suite.T().Run(strconv.FormatBool(v), func(t *testing.T) {
|
||||
testStruct := typeBoolTestStruct{
|
||||
Value: config.TypeBool{
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]bool{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBoolTestSuite) TestGet() {
|
||||
value := config.TypeBool{}
|
||||
suite.False(value.Get(false))
|
||||
suite.True(value.Get(true))
|
||||
|
||||
value.Value = true
|
||||
suite.True(value.Get(false))
|
||||
suite.True(value.Get(true))
|
||||
}
|
||||
|
||||
func TestTypeBool(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBoolTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
)
|
||||
|
||||
var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB")
|
||||
|
||||
type TypeBytes struct {
|
||||
Value units.Base2Bytes
|
||||
}
|
||||
|
||||
func (t *TypeBytes) Set(value string) error {
|
||||
normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value))
|
||||
|
||||
parsedValue, err := units.ParseBase2Bytes(normalizedValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect bytes value (%v): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedValue < 0 {
|
||||
return fmt.Errorf("bytes should be positive (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = parsedValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeBytes) Get(defaultValue uint) uint {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return uint(t.Value)
|
||||
}
|
||||
|
||||
func (t *TypeBytes) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeBytes) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeBytes) String() string {
|
||||
if t.Value == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(t.Value.String())
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeBytesTestStruct struct {
|
||||
Value config.TypeBytes `json:"value"`
|
||||
}
|
||||
|
||||
type TypeBytesTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"1m",
|
||||
"1",
|
||||
"-1kb",
|
||||
"-1kib",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]uint{
|
||||
"1b": 1,
|
||||
"1kb": 1024,
|
||||
"1kib": 1024,
|
||||
"2mb": 2 * 1024 * 1024,
|
||||
"2mib": 2 * 1024 * 1024,
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeBytesTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.EqualValues(t, value, testStruct.Value.Get(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestMarshalOk() {
|
||||
value := typeBytesTestStruct{}
|
||||
suite.NoError(value.Value.Set("1kib"))
|
||||
|
||||
data, err := json.Marshal(value)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "1kib"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeBytesTestSuite) TestGet() {
|
||||
value := config.TypeBytes{}
|
||||
suite.EqualValues(1000, value.Get(1000))
|
||||
|
||||
suite.NoError(value.Set("1mib"))
|
||||
suite.EqualValues(1048576, value.Get(1000))
|
||||
}
|
||||
|
||||
func TestTypeBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeBytesTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeConcurrency struct {
|
||||
Value uint
|
||||
}
|
||||
|
||||
func (t *TypeConcurrency) Set(value string) error {
|
||||
concurrencyValue, err := strconv.ParseUint(value, 10, 16) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not uint (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if concurrencyValue == 0 {
|
||||
return fmt.Errorf("value should be >0 (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = uint(concurrencyValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeConcurrency) Get(defaultValue uint) uint {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeConcurrency) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeConcurrency) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeConcurrency) String() string {
|
||||
return strconv.FormatUint(uint64(t.Value), 10) // nolint: gomnd
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeConcurrencyTestStruct struct {
|
||||
Value config.TypeConcurrency `json:"value"`
|
||||
}
|
||||
|
||||
type TypeConcurrencyTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"-1",
|
||||
"0",
|
||||
"0.0",
|
||||
"1.0",
|
||||
"1.1",
|
||||
".",
|
||||
"some_value",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeConcurrencyTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() {
|
||||
testStruct := &typeConcurrencyTestStruct{}
|
||||
|
||||
suite.NoError(json.Unmarshal([]byte(`{"value": 1}`), testStruct))
|
||||
suite.EqualValues(1, testStruct.Value.Get(2))
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestMarshalOk() {
|
||||
testStruct := &typeConcurrencyTestStruct{
|
||||
Value: config.TypeConcurrency{
|
||||
Value: 2,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": 2}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeConcurrencyTestSuite) TestGet() {
|
||||
value := config.TypeConcurrency{}
|
||||
suite.EqualValues(1, value.Get(1))
|
||||
|
||||
value.Value = 3
|
||||
suite.EqualValues(3, value.Get(1))
|
||||
}
|
||||
|
||||
func TestTypeConcurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeConcurrencyTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "")
|
||||
|
||||
type TypeDuration struct {
|
||||
Value time.Duration
|
||||
}
|
||||
|
||||
func (t *TypeDuration) Set(value string) error {
|
||||
parsedValue, err := time.ParseDuration(
|
||||
typeDurationStringCleaner.Replace(strings.ToLower(value)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect duration (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedValue < 0 {
|
||||
return fmt.Errorf("duration has to be a positive: %s", value)
|
||||
}
|
||||
|
||||
t.Value = parsedValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeDuration) Get(defaultValue time.Duration) time.Duration {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeDuration) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeDuration) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeDuration) String() string {
|
||||
if t.Value == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Value.String()
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeDurationTestStruct struct {
|
||||
Value config.TypeDuration `json:"value"`
|
||||
}
|
||||
|
||||
type TypeDurationTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"-1s",
|
||||
"1 seconds ago",
|
||||
"1s ago",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeDurationTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]time.Duration{
|
||||
"1s": time.Second,
|
||||
"0": 0 * time.Second,
|
||||
"0s": 0 * time.Second,
|
||||
"1\tM": time.Minute,
|
||||
"1H": time.Hour,
|
||||
"1 h": time.Hour,
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeDurationTestStruct{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestMarshalOk() {
|
||||
testData := map[string]string{
|
||||
"1s": "1s",
|
||||
"0": "",
|
||||
"0s": "",
|
||||
"0ms": "",
|
||||
"1 H": "1h0m0s",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := k
|
||||
expected := v
|
||||
|
||||
suite.T().Run(value, func(t *testing.T) {
|
||||
testStruct := &typeDurationTestStruct{}
|
||||
|
||||
assert.NoError(t, testStruct.Value.Set(value))
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": expected,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeDurationTestSuite) TestGet() {
|
||||
value := config.TypeDuration{}
|
||||
suite.Equal(time.Second, value.Get(time.Second))
|
||||
|
||||
value.Value = 3 * time.Second
|
||||
suite.Equal(3*time.Second, value.Get(time.Hour))
|
||||
}
|
||||
|
||||
func TestTypeDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeDurationTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const typeErrorRateIgnoreLess = 1e-8
|
||||
|
||||
type TypeErrorRate struct {
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (t *TypeErrorRate) Set(value string) error {
|
||||
parsedValue, err := strconv.ParseFloat(value, 64) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not a float (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedValue <= 0.0 || parsedValue >= 100.0 {
|
||||
return fmt.Errorf("value should be 0 < x < 100 (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = parsedValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeErrorRate) Get(defaultValue float64) float64 {
|
||||
if t.Value < typeErrorRateIgnoreLess {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeErrorRate) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeErrorRate) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeErrorRate) String() string {
|
||||
return strconv.FormatFloat(t.Value, 'f', -1, 64) // nolint: gomnd
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeErrorRateTestStruct struct {
|
||||
Value config.TypeErrorRate `json:"value"`
|
||||
}
|
||||
|
||||
type TypeErrorRateTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"1s",
|
||||
"1,",
|
||||
"1,2",
|
||||
".",
|
||||
"3.4.5",
|
||||
"3.5.",
|
||||
".3.5",
|
||||
"some word",
|
||||
"1e2",
|
||||
"-1.0",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() {
|
||||
data, err := json.Marshal(map[string]float64{
|
||||
"value": 1.0,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
testStruct := &typeErrorRateTestStruct{}
|
||||
suite.NoError(json.Unmarshal(data, testStruct))
|
||||
suite.InEpsilon(1.0, testStruct.Value.Value, 1e-10)
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestMarshalOk() {
|
||||
testStruct := typeErrorRateTestStruct{
|
||||
Value: config.TypeErrorRate{
|
||||
Value: 1.01,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": 1.01}`, string(encodedJSON))
|
||||
}
|
||||
|
||||
func (suite *TypeErrorRateTestSuite) TestGet() {
|
||||
value := config.TypeErrorRate{}
|
||||
suite.InEpsilon(1.0, value.Get(1.0), 1e-10)
|
||||
|
||||
value.Value = 5.0
|
||||
suite.InEpsilon(5.0, value.Get(1.0), 1e-10)
|
||||
}
|
||||
|
||||
func TestTypeErrorRate(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeErrorRateTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypeHostPort struct {
|
||||
Value string
|
||||
Host string
|
||||
Port uint
|
||||
}
|
||||
|
||||
func (t *TypeHostPort) Set(value string) error {
|
||||
host, port, err := net.SplitHostPort(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect host:port value (%v): %w", value, err)
|
||||
}
|
||||
|
||||
portValue, err := strconv.ParseUint(port, 10, 16) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect port number (%v): %w", value, err)
|
||||
}
|
||||
|
||||
if portValue == 0 {
|
||||
return fmt.Errorf("incorrect port number (%s)", value)
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return fmt.Errorf("empty host: %s", value)
|
||||
}
|
||||
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("host is not an IP address: %s", value)
|
||||
}
|
||||
|
||||
t.Value = net.JoinHostPort(host, port)
|
||||
t.Port = uint(portValue)
|
||||
t.Host = host
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeHostPort) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeHostPort) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeHostPort) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeHostPort) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeHostPortTestStruct struct {
|
||||
Value config.TypeHostPort `json:"value"`
|
||||
}
|
||||
|
||||
type TypeHostPortTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
":",
|
||||
":800",
|
||||
"127.0.0.1:8000000",
|
||||
"12...:80",
|
||||
"",
|
||||
"localhost",
|
||||
"google.com:",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeHostPortTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
"127.0.0.1:80",
|
||||
"10.0.0.10:6553",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeHostPortTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestMarshalOk() {
|
||||
testStruct := typeHostPortTestStruct{
|
||||
Value: config.TypeHostPort{
|
||||
Value: "127.0.0.1:8000",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeHostPortTestSuite) TestGet() {
|
||||
value := config.TypeHostPort{}
|
||||
suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000"))
|
||||
|
||||
value.Value = "127.0.0.1:80"
|
||||
suite.Equal("127.0.0.1:80", value.Get("127.0.0.1:9000"))
|
||||
}
|
||||
|
||||
func TestTypeHostPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeHostPortTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
type TypeHTTPPath struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeHTTPPath) Set(value string) error {
|
||||
t.Value = "/" + strings.Trim(value, "/")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeHTTPPath) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeHTTPPath) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeHTTPPath) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeHTTPPath) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeHTTPPathTestStruct struct {
|
||||
Value config.TypeHTTPPath `json:"value"`
|
||||
}
|
||||
|
||||
type TypeHTTPPathTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"": "/",
|
||||
"/": "/",
|
||||
"/path": "/path",
|
||||
"path": "/path",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeHTTPPathTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, value, testStruct.Value.Get(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestMarshalOk() {
|
||||
value := typeHTTPPathTestStruct{
|
||||
Value: config.TypeHTTPPath{
|
||||
Value: "/path",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(value)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "/path"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeHTTPPathTestSuite) TestGet() {
|
||||
value := config.TypeHTTPPath{}
|
||||
suite.Equal("/hello", value.Get("/hello"))
|
||||
|
||||
suite.NoError(value.Set("/lalala"))
|
||||
suite.Equal("/lalala", value.Get("/hello"))
|
||||
}
|
||||
|
||||
func TestTypeHTTPPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeHTTPPathTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type TypeIP struct {
|
||||
Value net.IP
|
||||
}
|
||||
|
||||
func (t *TypeIP) Set(value string) error {
|
||||
ip := net.ParseIP(value)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("incorret ip %s", value)
|
||||
}
|
||||
|
||||
t.Value = ip
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TypeIP) Get(defaultValue net.IP) net.IP {
|
||||
if len(t.Value) == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeIP) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeIP) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeIP) String() string {
|
||||
if len(t.Value) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Value.String()
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeIPTestStruct struct {
|
||||
Value config.TypeIP `json:"value"`
|
||||
}
|
||||
|
||||
type TypeIPTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"....",
|
||||
"0...",
|
||||
"300.200.200.800",
|
||||
"[]",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeIPTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "2001:db8:85a3::8a2e:370:7334",
|
||||
"127.0.0.1": "127.0.0.1",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
expected := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeIPTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, expected, testStruct.Value.Get(nil).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
"2001:db8:85a3::8a2e:370:7334",
|
||||
"127.0.0.1",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeIPTestStruct{
|
||||
Value: config.TypeIP{
|
||||
Value: net.ParseIP(value),
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeIPTestSuite) TestGet() {
|
||||
value := config.TypeIP{}
|
||||
suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).String())
|
||||
|
||||
suite.NoError(value.Set("127.0.0.2"))
|
||||
suite.Equal("127.0.0.2", value.Get(net.ParseIP("127.0.0.1")).String())
|
||||
}
|
||||
|
||||
func TestTypeIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeIPTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type TypeMetricPrefix struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeMetricPrefix) Set(value string) error {
|
||||
if ok, err := regexp.MatchString("^[a-z0-9]+$", value); !ok || err != nil {
|
||||
return fmt.Errorf("incorrect metric prefix %s: %w", value, err)
|
||||
}
|
||||
|
||||
t.Value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypeMetricPrefix) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeMetricPrefix) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeMetricPrefix) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeMetricPrefix) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeMetricPrefixTestStruct struct {
|
||||
Value config.TypeMetricPrefix `json:"value"`
|
||||
}
|
||||
|
||||
type TypeMetricPrefixTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"-",
|
||||
"hello/world",
|
||||
"lala*",
|
||||
"++sdf++",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeMetricPrefixTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() {
|
||||
testStruct := &typeMetricPrefixTestStruct{}
|
||||
suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct))
|
||||
suite.Equal("mtg", testStruct.Value.Get("lalala"))
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() {
|
||||
testStruct := &typeMetricPrefixTestStruct{
|
||||
Value: config.TypeMetricPrefix{
|
||||
Value: "mtg",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "mtg"}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypeMetricPrefixTestSuite) TestGet() {
|
||||
value := config.TypeMetricPrefix{}
|
||||
suite.Equal("lalala", value.Get("lalala"))
|
||||
|
||||
value.Value = "mtg"
|
||||
suite.Equal("mtg", value.Get("lalala"))
|
||||
}
|
||||
|
||||
func TestTypeMetricPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypeMetricPrefixTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TypePort struct {
|
||||
Value uint
|
||||
}
|
||||
|
||||
func (t *TypePort) Set(value string) error {
|
||||
portValue, err := strconv.ParseUint(value, 10, 16) // nolint: gomnd
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect port number (%v): %w", value, err)
|
||||
}
|
||||
|
||||
if portValue == 0 {
|
||||
return fmt.Errorf("incorrect port number (%s)", value)
|
||||
}
|
||||
|
||||
t.Value = uint(portValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TypePort) Get(defaultValue uint) uint {
|
||||
if t.Value == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypePort) UnmarshalJSON(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypePort) MarshalJSON() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypePort) String() string {
|
||||
return strconv.Itoa(int(t.Value))
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typePortTestStruct struct {
|
||||
Value config.TypePort `json:"value"`
|
||||
}
|
||||
|
||||
type TypePortTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"port",
|
||||
"0",
|
||||
"-1",
|
||||
"1.5",
|
||||
"70000",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typePortTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestUnmarshalOk() {
|
||||
testStruct := &typePortTestStruct{}
|
||||
suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct))
|
||||
suite.EqualValues(5, testStruct.Value.Value)
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestMarshalOk() {
|
||||
testStruct := &typePortTestStruct{
|
||||
Value: config.TypePort{
|
||||
Value: 10,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value":10}`, string(data))
|
||||
}
|
||||
|
||||
func (suite *TypePortTestSuite) TestGet() {
|
||||
value := config.TypePort{}
|
||||
suite.EqualValues(10, value.Get(10))
|
||||
|
||||
value.Value = 100
|
||||
suite.EqualValues(100, value.Get(10))
|
||||
}
|
||||
|
||||
func TestTypePort(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypePortTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypePreferIPPreferIPv4 states that you prefer to use IPv4 addresses
|
||||
// but IPv6 is also possible.
|
||||
TypePreferIPPreferIPv4 = "prefer-ipv4"
|
||||
|
||||
// TypePreferIPPreferIPv6 states that you prefer to use IPv6 addresses
|
||||
// but IPv4 is also possible.
|
||||
TypePreferIPPreferIPv6 = "prefer-ipv6"
|
||||
|
||||
// TypePreferOnlyIPv4 states that you prefer to use IPv4 addresses
|
||||
// only.
|
||||
TypePreferOnlyIPv4 = "only-ipv4"
|
||||
|
||||
// TypePreferOnlyIPv6 states that you prefer to use IPv6 addresses
|
||||
// only.
|
||||
TypePreferOnlyIPv6 = "only-ipv6"
|
||||
)
|
||||
|
||||
type TypePreferIP struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypePreferIP) Set(value string) error {
|
||||
value = strings.ToLower(value)
|
||||
|
||||
switch value {
|
||||
case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6,
|
||||
TypePreferOnlyIPv4, TypePreferOnlyIPv6:
|
||||
t.Value = value
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported ip preference: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TypePreferIP) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypePreferIP) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypePreferIP) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypePreferIP) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typePreferIPTestStruct struct {
|
||||
Value config.TypePreferIP `json:"value"`
|
||||
}
|
||||
|
||||
type TypePreferIPTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"prefer",
|
||||
"preferipv4",
|
||||
config.TypePreferIPPreferIPv4 + "_",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typePreferIPTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
config.TypePreferIPPreferIPv4,
|
||||
config.TypePreferIPPreferIPv6,
|
||||
config.TypePreferOnlyIPv4,
|
||||
config.TypePreferOnlyIPv6,
|
||||
strings.ToTitle(config.TypePreferOnlyIPv4),
|
||||
strings.ToTitle(config.TypePreferOnlyIPv6),
|
||||
strings.ToTitle(config.TypePreferIPPreferIPv4),
|
||||
strings.ToTitle(config.TypePreferIPPreferIPv6),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typePreferIPTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, strings.ToLower(value), testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
config.TypePreferIPPreferIPv4,
|
||||
config.TypePreferIPPreferIPv6,
|
||||
config.TypePreferOnlyIPv4,
|
||||
config.TypePreferOnlyIPv6,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typePreferIPTestStruct{
|
||||
Value: config.TypePreferIP{
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TypePreferIPTestSuite) TestGet() {
|
||||
value := config.TypePreferIP{}
|
||||
suite.Equal(config.TypePreferIPPreferIPv4,
|
||||
value.Get(config.TypePreferIPPreferIPv4))
|
||||
|
||||
suite.NoError(value.Set(config.TypePreferIPPreferIPv6))
|
||||
suite.Equal(config.TypePreferIPPreferIPv6,
|
||||
value.Get(config.TypePreferIPPreferIPv4))
|
||||
}
|
||||
|
||||
func TestTypePreferIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &TypePreferIPTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const typeProxyURLDefaultSOCKS5Port = "1080"
|
||||
|
||||
type TypeProxyURL struct {
|
||||
Value *url.URL
|
||||
}
|
||||
|
||||
func (t *TypeProxyURL) Set(value string) error {
|
||||
parsedURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not corect URL (%s): %w", value, err)
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("url has to have a schema: %s", value)
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "socks5" {
|
||||
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil {
|
||||
parsedURL.Host = net.JoinHostPort(parsedURL.Host,
|
||||
typeProxyURLDefaultSOCKS5Port)
|
||||
}
|
||||
|
||||
t.Value = parsedURL
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TypeProxyURL) Get(defaultValue *url.URL) *url.URL {
|
||||
if t.Value == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeProxyURL) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t TypeProxyURL) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t TypeProxyURL) String() string {
|
||||
if t.Value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Value.String()
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeProxyURLTestStruct struct {
|
||||
Value config.TypeProxyURL `json:"value"`
|
||||
}
|
||||
|
||||
type ProxyURLTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"socks5://",
|
||||
"://lala",
|
||||
"/path",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeProxyURLTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestUnmarshalOk() {
|
||||
testData := map[string]string{
|
||||
"socks5://127.0.0.1/?open_threshold=1": "socks5://127.0.0.1:1080/?open_threshold=1",
|
||||
"socks5://127.0.0.1:80": "socks5://127.0.0.1:80",
|
||||
}
|
||||
|
||||
for k, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": k,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(k, func(t *testing.T) {
|
||||
testStruct := &typeProxyURLTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
|
||||
parsed, _ := url.Parse(value)
|
||||
|
||||
assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme)
|
||||
assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host)
|
||||
assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery)
|
||||
assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestMarshalOk() {
|
||||
parsed, _ := url.Parse("socks5://127.0.0.1:1080?open_threshold=1")
|
||||
testStruct := &typeProxyURLTestStruct{
|
||||
Value: config.TypeProxyURL{
|
||||
Value: parsed,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
suite.NoError(err)
|
||||
suite.JSONEq(`{"value": "socks5://127.0.0.1:1080?open_threshold=1"}`,
|
||||
string(encodedJSON))
|
||||
}
|
||||
|
||||
func (suite *ProxyURLTestSuite) TestGet() {
|
||||
emptyURL := &url.URL{}
|
||||
|
||||
value := config.TypeProxyURL{}
|
||||
suite.Equal(emptyURL, value.Get(emptyURL))
|
||||
|
||||
value.Value = &url.URL{}
|
||||
suite.Equal(value.Value, value.Get(emptyURL))
|
||||
}
|
||||
|
||||
func TestTypeProxyURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &ProxyURLTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeStatsdTagFormatInfluxdb defines a tag format compatible with
|
||||
// InfluxDB.
|
||||
TypeStatsdTagFormatInfluxdb = "influxdb"
|
||||
|
||||
// TypeStatsdTagFormatDatadog defines a tag format compatible with
|
||||
// DataDog.
|
||||
TypeStatsdTagFormatDatadog = "datadog"
|
||||
|
||||
// TypeStatsdTagFormatGraphite defines a tag format compatible with
|
||||
// Graphite.
|
||||
TypeStatsdTagFormatGraphite = "graphite"
|
||||
)
|
||||
|
||||
type TypeStatsdTagFormat struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) Set(value string) error {
|
||||
lowercasedValue := strings.ToLower(value)
|
||||
|
||||
switch lowercasedValue {
|
||||
case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb,
|
||||
TypeStatsdTagFormatGraphite:
|
||||
t.Value = lowercasedValue
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown tag format %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (t TypeStatsdTagFormat) Get(defaultValue string) string {
|
||||
if t.Value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return t.Value
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) UnmarshalText(data []byte) error {
|
||||
return t.Set(string(data))
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t *TypeStatsdTagFormat) String() string {
|
||||
return t.Value
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type typeStatsdTagFormatTestStruct struct {
|
||||
Value config.TypeStatsdTagFormat `json:"value"`
|
||||
}
|
||||
|
||||
type StatsdTagFormatTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() {
|
||||
testData := []string{
|
||||
"",
|
||||
"dogdog",
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() {
|
||||
testData := []string{
|
||||
config.TypeStatsdTagFormatInfluxdb,
|
||||
config.TypeStatsdTagFormatGraphite,
|
||||
config.TypeStatsdTagFormatDatadog,
|
||||
strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatGraphite),
|
||||
strings.ToUpper(config.TypeStatsdTagFormatDatadog),
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"value": v,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeStatsdTagFormatTestStruct{}
|
||||
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||
assert.Equal(t, strings.ToLower(value), testStruct.Value.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestMarshalOk() {
|
||||
testData := []string{
|
||||
config.TypeStatsdTagFormatInfluxdb,
|
||||
config.TypeStatsdTagFormatGraphite,
|
||||
config.TypeStatsdTagFormatDatadog,
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
value := v
|
||||
|
||||
suite.T().Run(v, func(t *testing.T) {
|
||||
testStruct := &typeStatsdTagFormatTestStruct{
|
||||
Value: config.TypeStatsdTagFormat{
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(testStruct)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedJSON, err := json.Marshal(map[string]string{
|
||||
"value": value,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatsdTagFormatTestSuite) TestGet() {
|
||||
value := config.TypeStatsdTagFormat{}
|
||||
suite.Equal(config.TypeStatsdTagFormatDatadog,
|
||||
value.Get(config.TypeStatsdTagFormatDatadog))
|
||||
|
||||
suite.NoError(value.Set(config.TypeStatsdTagFormatInfluxdb))
|
||||
suite.Equal(config.TypeStatsdTagFormatInfluxdb,
|
||||
value.Get(config.TypeStatsdTagFormatDatadog))
|
||||
}
|
||||
|
||||
func TestTypeStatsdTagFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
suite.Run(t, &StatsdTagFormatTestSuite{})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CaptureStdout(callback func()) string {
|
||||
return captureOutput(&os.Stdout, callback)
|
||||
}
|
||||
|
||||
func CaptureStderr(callback func()) string {
|
||||
return captureOutput(&os.Stderr, callback)
|
||||
}
|
||||
|
||||
func captureOutput(filefp **os.File, callback func()) string {
|
||||
oldFp := *filefp
|
||||
|
||||
defer func() {
|
||||
*filefp = oldFp
|
||||
}()
|
||||
|
||||
reader, writer, _ := os.Pipe()
|
||||
buf := &bytes.Buffer{}
|
||||
closeChan := make(chan bool)
|
||||
|
||||
go func() {
|
||||
io.Copy(buf, reader) // nolint: errcheck
|
||||
close(closeChan)
|
||||
}()
|
||||
|
||||
*filefp = writer
|
||||
|
||||
callback()
|
||||
|
||||
writer.Close()
|
||||
<-closeChan
|
||||
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package testlib
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type EventsObserverMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package testlib
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type MtglibAntiReplayCacheMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MtglibAntiReplayCacheMock) SeenBefore(data []byte) bool {
|
||||
return m.Called(data).Bool(0)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/9seconds/mtg/v2/essentials"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MtglibNetworkMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) Dial(network, address string) (essentials.Conn, error) {
|
||||
args := m.Called(network, address)
|
||||
|
||||
return args.Get(0).(essentials.Conn), args.Error(1) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
|
||||
args := m.Called(ctx, network, address)
|
||||
|
||||
return args.Get(0).(essentials.Conn), args.Error(1) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
|
||||
network, address string) (essentials.Conn, error)) *http.Client {
|
||||
return m.Called(dialFunc).Get(0).(*http.Client)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package testlib
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type EssentialsConnMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) Read(b []byte) (int, error) {
|
||||
args := n.Called(b)
|
||||
|
||||
return args.Int(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) Write(b []byte) (int, error) {
|
||||
args := n.Called(b)
|
||||
|
||||
return args.Int(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) Close() error {
|
||||
return n.Called().Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) CloseRead() error {
|
||||
return n.Called().Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) CloseWrite() error {
|
||||
return n.Called().Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) LocalAddr() net.Addr {
|
||||
return n.Called().Get(0).(net.Addr)
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) RemoteAddr() net.Addr {
|
||||
return n.Called().Get(0).(net.Addr)
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) SetDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) SetReadDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
|
||||
func (n *EssentialsConnMock) SetWriteDeadline(t time.Time) error {
|
||||
return n.Called(t).Error(0) // nolint: wrapcheck
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import "net/url"
|
||||
|
||||
func MakeQRCodeURL(data string) string {
|
||||
values := url.Values{}
|
||||
values.Set("qzone", "4")
|
||||
values.Set("format", "svg")
|
||||
values.Set("data", data)
|
||||
|
||||
rv := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.qrserver.com",
|
||||
Path: "/v1/create-qr-code",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
return rv.String()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user