├── .github
└── workflows
│ ├── build-releases.yml
│ └── docker_push.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README-EN.md
├── README.md
├── cli.go
├── cli_misc.go
├── cli_script.go
├── cli_server.go
├── docker
├── Dockerfile
└── install.sh
├── engine
├── embeded.go
├── embeded
│ ├── default_geoip.js
│ ├── default_ip.js
│ └── predefined.js
├── engine.go
├── factory
│ ├── fetch.go
│ ├── netcat.go
│ └── print.go
└── helpers
│ └── helpers.go
├── go.mod
├── go.sum
├── interfaces
├── api_request.go
├── api_request_config.go
├── api_response.go
├── geoip.go
├── macro.go
├── macro_fields.go
├── matrix.go
├── matrix_fields.go
├── misc.go
├── proxy.go
├── scripts.go
├── utils.go
└── vendor.go
├── main.go
├── misc.go
├── preconfigs
├── certs.go
├── embeded.go
├── embeded
│ ├── .gitkeep
│ └── ca-certificates.crt
└── network.go
├── service
├── macros
│ ├── geo
│ │ ├── engine.go
│ │ ├── geo.go
│ │ ├── geocheck.go
│ │ ├── macro.go
│ │ └── mmdb.go
│ ├── invalid
│ │ └── macro.go
│ ├── macros.go
│ ├── ping
│ │ ├── macro.go
│ │ ├── ping.go
│ │ └── utils.go
│ ├── script
│ │ ├── engine.go
│ │ └── macro.go
│ ├── sleep
│ │ └── macro.go
│ ├── speed
│ │ ├── macro.go
│ │ ├── speed.go
│ │ └── writer.go
│ └── udp
│ │ ├── macro.go
│ │ ├── nat.go
│ │ └── udp.go
├── matrices
│ ├── averagespeed
│ │ └── matrix.go
│ ├── debug
│ │ └── matrix.go
│ ├── httpping
│ │ └── matrix.go
│ ├── httpstatuscode
│ │ └── matrix.go
│ ├── inboundgeoip
│ │ └── matrix.go
│ ├── invalid
│ │ └── matrix.go
│ ├── matrices.go
│ ├── maxrttping
│ │ └── matrix.go
│ ├── maxspeed
│ │ └── matrix.go
│ ├── outboundgeoip
│ │ └── matrix.go
│ ├── packetloss
│ │ └── matrix.go
│ ├── persecondspeed
│ │ └── matrix.go
│ ├── rttping
│ │ └── matrix.go
│ ├── scripttest
│ │ └── matrix.go
│ ├── sdhttp
│ │ └── matrix.go
│ ├── sdrtt
│ │ └── matrix.go
│ ├── totalrttping
│ │ └── matrix.go
│ └── udptype
│ │ └── matrix.go
├── runner.go
├── server.go
├── service.go
├── task.go
├── taskpoll
│ ├── controller.go
│ └── item.go
└── testingpollitem.go
├── utils
├── archive.go
├── challenge.go
├── config.go
├── constants.go
├── dns.go
├── embeded
│ └── .gitkeep
├── ipfliter
│ └── ipfliter.go
├── logger.go
├── maxmind.go
├── network.go
├── stats.go
├── structs
│ ├── asyncarr.go
│ ├── asyncmap.go
│ ├── helper.go
│ ├── ipfliter.go
│ ├── memutils
│ │ ├── driver.go
│ │ └── driver_memory.go
│ ├── misc.go
│ ├── obliviousmap
│ │ └── obliviousmap.go
│ └── set.go
├── sys.go
└── utils.go
└── vendors
├── clash
├── metadata.go
├── profile.go
└── vendor.go
├── commons.go
├── invalid
└── vendor.go
├── local
├── metadata.go
└── vendor.go
└── vendors.go
/.github/workflows/build-releases.yml:
--------------------------------------------------------------------------------
1 | name: build and releases
2 | on:
3 | push:
4 | branches:
5 | - "build"
6 | workflow_dispatch:
7 |
8 |
9 | jobs:
10 | go-build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v4
16 |
17 | - name: Get the tag
18 | run: echo "The tag is ${{ github.ref_name }}"
19 |
20 | - uses: actions/setup-go@v4
21 | with:
22 | go-version: '1.21'
23 | cache: true
24 |
25 | - name: fix embeded
26 | run: |
27 | go env
28 | mkdir ./preconfigs/embeded/miaokoCA
29 | echo "${{ secrets.MIAOSPEED_CRT }}" > ./preconfigs/embeded/miaokoCA/miaoko.crt
30 | echo "${{ secrets.MIAOSPEED_KEY }}" > ./preconfigs/embeded/miaokoCA/miaoko.key
31 | echo "${{ secrets.MS_BUILDTOKEN }}" > ./utils/embeded/BUILDTOKEN.key
32 | head -n 5 ./engine/embeded/default_geoip.js
33 |
34 | - name: build
35 | run: make releases
36 |
37 | - name: Release
38 | uses: softprops/action-gh-release@v2
39 | with:
40 | files: ./bin/*
41 | body_path: './CHANGELOG.md'
42 | tag_name: ${{ github.ref_name }}
43 |
44 | - name: Get the tag
45 | run: |
46 | echo "MIAOSPEED_TAG=$(curl -s https://api.github.com/repos/AirportR/miaospeed/releases/latest | grep 'tag_name' | cut -d '"' -f 4)" >> $GITHUB_ENV
47 | echo "The tag is ${{ github.ref_name }}"
48 |
49 | - name: Set up QEMU
50 | uses: docker/setup-qemu-action@v3
51 |
52 | - name: Set up Docker Buildx
53 | uses: docker/setup-buildx-action@v3
54 |
55 | - name: Log in to Docker Hub
56 | uses: docker/login-action@v3
57 | with:
58 | username: ${{ secrets.DOCKERHUB_USERNAME }}
59 | password: ${{ secrets.DOCKERHUB_TOKEN }}
60 |
61 | - name: Build and push Docker image
62 | uses: docker/build-push-action@v4
63 | with:
64 | context: .
65 | file: ./docker/Dockerfile
66 | push: true
67 | tags: |
68 | ${{ secrets.DOCKERHUB_USERNAME }}/miaospeed:${{ env.MIAOSPEED_TAG }}
69 | ${{ secrets.DOCKERHUB_USERNAME }}/miaospeed:latest
70 | platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
71 |
72 | - name: Logout from Docker Hub
73 | run: docker logout
74 |
--------------------------------------------------------------------------------
/.github/workflows/docker_push.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - "docker"
8 |
9 | jobs:
10 | build-and-push:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v4
16 |
17 | - name: Get the tag
18 | run: |
19 | echo "MIAOSPEED_TAG=$(curl -s https://api.github.com/repos/AirportR/miaospeed/releases/latest | grep 'tag_name' | cut -d '"' -f 4)" >> $GITHUB_ENV
20 | echo "The tag is ${{ github.ref_name }}"
21 |
22 | - name: Set up QEMU
23 | uses: docker/setup-qemu-action@v3
24 |
25 | - name: Set up Docker Buildx
26 | uses: docker/setup-buildx-action@v3
27 |
28 | - name: Log in to Docker Hub
29 | uses: docker/login-action@v3
30 | with:
31 | username: ${{ secrets.DOCKERHUB_USERNAME }}
32 | password: ${{ secrets.DOCKERHUB_TOKEN }}
33 |
34 | - name: Build and push Docker image
35 | uses: docker/build-push-action@v4
36 | with:
37 | context: .
38 | file: ./docker/Dockerfile
39 | push: true
40 | tags: |
41 | ${{ secrets.DOCKERHUB_USERNAME }}/miaospeed:latest
42 | ${{ secrets.DOCKERHUB_USERNAME }}/miaospeed:${{ env.MIAOSPEED_TAG }}
43 | platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
44 |
45 | - name: Logout from Docker Hub
46 | run: docker logout
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # temp files
2 | temp.png
3 | goreleaser*
4 | # built artifacts
5 | miaoko
6 | miaospeed
7 | dist
8 | *.tgz
9 | *.dev.sh
10 | build.*.sh
11 | *.mmdb
12 | .idea/
13 |
14 | # configs
15 | test.yaml
16 | config.yaml
17 | .history.yaml
18 |
19 | # embeded
20 | */embeded/*
21 | !*/embeded/.gitkeep
22 | !*/embeded/predefined.js
23 |
24 | # intermediates
25 | emojis
26 | history
27 | fonts
28 |
29 | # miscs
30 | .DS_Store
31 | ._.DS_Store
32 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | miaospeed v4.5.8
2 |
3 | 1. 将mihomo升级到 v1.19.3 版本
4 | 2. 添加对 AnyTLS协议的支持
5 | 3. 修复了部分ping地址无法测出延迟的问题
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME=miaospeed
2 | BINDIR=bin
3 | VERSION=$(shell git describe --tags --abbrev=0 | head -n 1 || echo "Unknown")
4 | BUILDTIME=$(shell date -u '+%Y-%m-%d_%I:%M:%S%p(UTC%:z)')
5 | COMMIT=$(shell git rev-parse --short HEAD || echo "Unknown")
6 | GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "main.VERSION=$(VERSION)" \
7 | -X "main.COMPILATIONTIME=$(BUILDTIME)" \
8 | -X "main.COMMIT=$(COMMIT)" \
9 | -w -s -buildid='
10 |
11 | PLATFORM_LIST = \
12 | darwin-amd64 \
13 | darwin-amd64-v3 \
14 | darwin-arm64 \
15 | linux-386 \
16 | linux-amd64 \
17 | linux-amd64-v3 \
18 | linux-armv5 \
19 | linux-armv6 \
20 | linux-armv7 \
21 | linux-arm64 \
22 | linux-mips-softfloat \
23 | linux-mips-hardfloat \
24 | linux-mipsle-softfloat \
25 | linux-mipsle-hardfloat \
26 | linux-mips64 \
27 | linux-mips64le \
28 | linux-riscv64 \
29 | linux-loong64 \
30 | freebsd-386 \
31 | freebsd-amd64 \
32 | freebsd-amd64-v3 \
33 | freebsd-arm64
34 |
35 | WINDOWS_ARCH_LIST = \
36 | windows-386 \
37 | windows-amd64 \
38 | windows-amd64-v3 \
39 | windows-arm64 \
40 | windows-armv7
41 |
42 | all: linux-amd64 linux-386 darwin-amd64 darwin-arm64 windows-amd64 windows-386 windows-arm64 linux-arm64 linux-armv7 linux-riscv64 linux-loong64 freebsd-amd64 freebsd-arm64# Most used
43 |
44 | darwin-amd64:
45 | GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
46 |
47 | darwin-amd64-v3:
48 | GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
49 |
50 | darwin-arm64:
51 | GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
52 |
53 | linux-386:
54 | GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
55 |
56 | linux-amd64:
57 | GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
58 |
59 | linux-amd64-v3:
60 | GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
61 |
62 | linux-armv5:
63 | GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
64 |
65 | linux-armv6:
66 | GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
67 |
68 | linux-armv7:
69 | GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
70 |
71 | linux-arm64:
72 | GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
73 |
74 | linux-mips-softfloat:
75 | GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
76 |
77 | linux-mips-hardfloat:
78 | GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
79 |
80 | linux-mipsle-softfloat:
81 | GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
82 |
83 | linux-mipsle-hardfloat:
84 | GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
85 |
86 | linux-mips64:
87 | GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
88 |
89 | linux-mips64le:
90 | GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
91 |
92 | linux-riscv64:
93 | GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
94 |
95 | linux-loong64:
96 | GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
97 |
98 | freebsd-386:
99 | GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
100 |
101 | freebsd-amd64:
102 | GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
103 |
104 | freebsd-amd64-v3:
105 | GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
106 |
107 | freebsd-arm64:
108 | GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
109 |
110 | windows-386:
111 | GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
112 |
113 | windows-amd64:
114 | GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
115 |
116 | windows-amd64-v3:
117 | GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
118 |
119 | windows-arm64:
120 | GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
121 |
122 | windows-armv7:
123 | GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
124 |
125 | gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
126 | zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
127 |
128 | #$(gz_releases): %.gz : %
129 | # chmod +x $(BINDIR)/$(NAME)-$(basename $@)
130 | # gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
131 | #
132 | #$(zip_releases): %.zip : %
133 | # zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
134 | $(gz_releases): %.gz : %
135 | chmod +x $(BINDIR)/$(NAME)-$(basename $@)
136 | tar -czf $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).tar.gz -C $(BINDIR) $(NAME)-$(basename $@) -C .. LICENSE
137 | rm $(BINDIR)/$(NAME)-$(basename $@)
138 |
139 | $(zip_releases): %.zip : %
140 | zip -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe LICENSE
141 | rm $(BINDIR)/$(NAME)-$(basename $@).exe
142 |
143 | all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
144 |
145 | releases: $(gz_releases) $(zip_releases)
146 |
147 | LINT_OS_LIST := darwin windows linux freebsd openbsd
148 |
149 | lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
150 | %-lint:
151 | GOOS=$* golangci-lint run ./...
152 |
153 | lint-fix: $(foreach os,$(LINT_OS_LIST),$(os)-lint-fix)
154 | %-lint-fix:
155 | GOOS=$* golangci-lint run --fix ./...
156 |
157 | clean:
158 | rm $(BINDIR)/*
159 |
--------------------------------------------------------------------------------
/README-EN.md:
--------------------------------------------------------------------------------
1 |
2 |
MiaoSpeed
3 |
🐱 High-performance proxy testing backend 🚀
4 |
English    简体中文
5 |

6 |

7 |

8 |
9 |

10 |

11 |
12 |

13 |
14 |
15 |
16 |
17 | ---
18 |
19 | ## Introduction
20 |
21 | ⚠️ This branch is a fork, as the original repository is no longer maintained by its developer.
22 |
23 | miaospeed is a performance testing tool for proxy servers, written in Go.
24 |
25 | ## Features
26 |
27 | - Cross-platform support for mainstream operating systems...
28 | - Proxy protocol implementation based on [Mihomo](https://github.com/MetaCubeX/Mihomo)
29 | - Backend service uses websocket protocol for full-duplex communication, supporting simultaneous testing by multiple clients.
30 | - Supports mainstream outbound proxies Shadowsocks, Vmess, VLESS, Trojan, Hysteria2, AnyTLS, etc.
31 | - Rich testing parameter configuration, supporting custom request body, timeout, concurrency, etc.
32 | - Custom javascript testing script support, supporting custom testing logic
33 |
34 |
35 | ## Basic Usage
36 |
37 | ### Pre-compiled Binary
38 |
39 | * Help information
40 | ```shell
41 | ./miaospeed
42 | ```
43 | * View version
44 | ```shell
45 | ./miaospeed -version
46 | ```
47 | * View websocket service help
48 | ```shell
49 | ./miaospeed server -help
50 | ```
51 | ### Compilation
52 |
53 | Since miaospeed was originally used in conjunction with the closed-source project **miaoko**, some certificates and scripts are not open source. You need to complete the following files to successfully compile:
54 |
55 | 1. `./utils/embeded/BUILDTOKEN.key`: This is the `build TOKEN`, which is used to sign the miaospeed **single test request** structure to prevent your client from using miaospeed in a non-standard way causing data authenticity disputes. You can define it freely, for example: `1111|2222|33333333`, different segments separated by `|`.
56 | 2. `./preconfigs/embeded/miaokoCA/miaoko.crt`: When `-mtls` is enabled, miaospeed will read the certificate here to let the client do TLS verification.
57 | 3. `./preconfigs/embeded/miaokoCA/miaoko.key`: Same as above, this is the private key. (For these two, you can sign a certificate with openssl yourself, but it cannot be used for miaoko.)
58 | 4. `./preconfigs/embeded/ca-certificates.crt`: The built-in root certificate set of miaospeed, preventing malicious users from modifying system certificates to fake TLS RTT. (For debian users, you can get this file at `/etc/ssl/certs/ca-certificates.crt` after installing the `ca-certificates` package)
59 |
60 | The following are **optional** operations:
61 |
62 | 1. `./engine/embeded/predefined.js`: This file defines some common methods in `JavaScript` (streaming media) scripts, such as `get()`, `safeStringify()`, `safeParse()`, `println()`. The default file is provided by the repository, and you can also modify it yourself.
63 | 2. `./engine/embeded/default_geoip.js`: Default `geoip` script, which needs to provide a `handler()` entry function. The default IP script is provided, and you can also modify it yourself.
64 | 3. `./engine/embeded/default_ip.js`: Default `ip_resolve` script, which needs to provide an `ip_resolve_default()` entry function to obtain the entry and exit IP. The default geoip script is provided, and you can also modify it yourself.
65 |
66 | After you have created the above files, you can run `go build .` to build `miaospeed`.
67 |
68 | ### Pre-compiled Binary Instructions
69 |
70 | The pre-compiled binaries in this repository are **compatible** with the original closed-source client Miaoko.
71 | The build token used is:
72 | ```
73 | MIAOKO4|580JxAo049R|GEnERAl|1X571R930|T0kEN
74 | ```
75 | The corresponding TLS certificate cannot be provided here due to closed-source reasons. If you want to compile miaospeed yourself for the closed-source client Miaoko, please extract the certificate from the original repository's pre-compiled binary through special means.
76 | If compatibility is not required, you may safely ignore this note.
77 |
78 | ### Integration Method
79 |
80 | If you want to integrate miaospeed in other services, please refer to the client implementations listed below. You can also refer to the following approach:
81 |
82 | 1. The essence of miaospeed integration is to send commands and pass information through the websocket (ws) channel. Generally, you only need to connect to ws, construct a request structure, sign the request, and receive the results.
83 | 2. Connect to ws, this step is simple and doesn't need elaboration. (If you forcibly disconnect the connection on the client side, the task will be automatically terminated)
84 | 3. Construct the request structure, reference: https://github.com/AirportR/miaospeed/blob/fd7abecc2d36a0f18b08f048f9a53b7c0a26bd9e/interfaces/api_request.go#L50
85 | 4. Signature, reference: https://github.com/AirportR/miaospeed/blob/df6202409e87c5d944ab756608fd31d35390b5c0/utils/challenge.go#L39 where two parameters need to be passed in. The first parameter is the `startup TOKEN` (that is, the content after -token when you start miaospeed), and the second is the structure `req` you constructed in the second step. To put it simply, the signature method is to convert the structure to a JSON String and then cumulatively perform SHA512 HASH with the `startup TOKEN` and `build TOKEN` slices respectively. Finally, write the signed string to `req.Challenge`.
86 | 5. After sending the signed request, you can receive the return value. The structure returned by the server is unified as https://github.com/AirportR/miaospeed/blob/fd7abecc2d36a0f18b08f048f9a53b7c0a26bd9e/interfaces/api_response.go#L28
87 |
88 |
89 | ## Main Client Implementations
90 |
91 | Since most of miaospeed’s functionality resides in the backend server, clients need to be implemented separately. You can refer to the following client implementations:
92 | * miaoko (closed-source) Project address: None
93 | * koipy (initially open-source, later closed-source) [Project address](https://github.com/koipy-org/koipy) [Project documentation](https://koipy.gitbook.io/koipy)
94 |
95 | If there are other client implementations in the future, feel free to contribute
96 |
97 | ## Copyright and License
98 |
99 | **miaospeed** is open-sourced under the AGPLv3 license. You can modify, contribute, distribute, and even use **miaospeed** commercially according to the AGPLv3 license. But please remember that you must comply with all obligations under the AGPLv3 license to avoid unnecessary legal disputes.
100 |
101 | ### Credits
102 |
103 | miaospeed uses the following open source projects:
104 |
105 | - Dreamacro/clash [GPLv3]
106 | - MetaCubeX/Clash.Meta [GPLv3]
107 | - juju/ratelimit [LGPLv3]
108 | - dop251/goja [MIT]
109 | - json-iterator/go [MIT]
110 | - pion/stun [MIT]
111 | - go-yaml/yaml [MIT]
112 | - gorilla/websocket [BSD]
113 | - jpillora/ipfilter [MIT]
114 |
115 | ## Abstract Design
116 |
117 | If you want to contribute to miaospeed, you can refer to the following abstract design of miaospeed:
118 |
119 | - **Matrix**: Data matrix [interfaces/matrix.go]. This is the smallest granularity of data that users want to obtain. For example, if a user wants to understand the RTT delay of a node, they can request miaospeed to test `TEST_PING_RTT` [for example: service/matrices/httpping/matrix.go].
120 | - **Macro**: Runtime macro task [interfaces/macro.go]. If users want to run data matrices in batches, they often do repetitive things. For example, `TEST_PING_RTT` and `TEST_PING_HTTP` are mostly doing the same things. If two _Matrix_ are run independently, it will waste a lot of resources. Therefore, we define _Macro_ as the smallest granularity execution unit. The _Macro_ completes a series of time-consuming operations in parallel, and then the _Matrix_ will parse the data obtained from the _Macro_ run to fill its own content.
121 | - **Vendor**: Service provider [interfaces/vendor.go]. miaospeed itself is just a testing tool, **it has no proxy capabilities**. Therefore, _Vendor_ serves as an interface that provides connection capabilities to miaospeed.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
MiaoSpeed - 喵速
3 |
🐱 高性能代理测速后端 🚀
4 |
English 简体中文
5 |

6 |

7 |

8 |
9 |

10 |

11 |
12 |

13 |
14 |
15 |
16 |
17 | ---
18 |
19 | ## 简介
20 |
21 | ⚠️ 本分支为 fork,原仓库作者已不再维护。
22 |
23 | miaospeed 是一个使用 Go 编写的代理服务器性能测试解决方案。
24 |
25 | ## 特性
26 |
27 | - 跨平台支持,兼容主流操作系统(Windows、Linux、MacOS)。
28 | - 基于 [Mihomo](https://github.com/MetaCubeX/Mihomo) 的代理协议实现。
29 | - 后端服务使用 WebSocket 协议进行全双工通信,支持多个客户端同时测试。
30 | - 支持主流出站代理协议:Shadowsocks、Vmess、VLESS、Trojan、Hysteria2、AnyTLS 等。
31 | - 丰富的测试参数配置,支持自定义请求体、超时、并发数等。
32 | - 支持自定义 JavaScript 测试脚本,可实现自定义测试逻辑。
33 |
34 | ## 基本用法
35 |
36 | ### 预编译二进制
37 |
38 | * 查看帮助信息
39 | ```shell
40 | ./miaospeed
41 | ```
42 | * 查看版本
43 | ```
44 | ./miaospeed -version
45 | ```
46 | * 查看 websocket 服务用法
47 | ```
48 | ./miaospeed server -help
49 | ```
50 |
51 |
52 | ### 编译
53 |
54 | 由于 miaospeed 最初与闭源项目 miaoko 搭配使用,因此部分证书与脚本未开源。您需要补齐以下文件以成功编译:
55 |
56 | 1. `./utils/embeded/BUILDTOKEN.key`: 这是 `编译TOKEN`,用于给 miaospeed 的 单次测试请求 结构体签名,避免客户端以非标准方式使用 miaospeed 导致数据真实性争议。您可以随便定义它,例如: `1111|2222|33333333`,不同段用 `|` 切开。
57 | 2. `./preconfigs/embeded/miaokoCA/miaoko.crt`: 当 `-mtls` 启用时,miaospeed 会读取这里的证书让客户端做 TLS 验证。
58 | 3. `./preconfigs/embeded/miaokoCA/miaoko.key`: 同上,这是私钥。(对于这两个您可以自己用 openssl 签一个证书,但它不能用于 miaoko。)
59 | 4. `./preconfigs/embeded/ca-certificates.crt`: miaospeed 自带的根证书集,防止有恶意用户修改系统更证书以作假 TLS RTT。(对于 debian 用户,您可以在安装 `ca-certificates` 包后,在 `/etc/ssl/certs/ca-certificates.crt` 获取这个文件)
60 |
61 | 以下为可选文件:
62 |
63 | 1. `./engine/embeded/predefined.js`: 这个文件定义了 `JavaScript` (流媒体)脚本中一些通用方法,例如 `get()`, `safeStringify()`, `safeParse()`, `println()`,默认文件已提供,您也可以自行修改。
64 | 2. `./engine/embeded/default_geoip.js`: 默认的 `geoip` 脚本,需要提供一个 `handler()` 入口函数。默认ip脚本已提供,您也可以自行修改。
65 | 3. `./engine/embeded/default_ip.js`: 默认的 `ip_resolve` 脚本,需要提供一个 `ip_resolve_default()` 入口函数,用于获取入口、出口的 IP。默认geoip脚本已提供,您也可以自行修改。
66 |
67 | 当您新建好以上文件后,就可以运行 `go build .` 构建 `miaospeed` 了。
68 |
69 | ### 预编译二进制说明
70 |
71 | 仓库提供的预编译二进制文件与闭源客户端 **Miaoko 兼容**。
72 | 所使用的 build token 为:
73 | ```
74 | MIAOKO4|580JxAo049R|GEnERAl|1X571R930|T0kEN
75 | ```
76 |
77 | 由于Miaoko闭源,对应的 TLS 证书无法提供。如果您需要自行编译一个可与 Miaoko 兼容的版本,请通过特殊方式从原仓库的预编译二进制中提取证书。
78 | 若不需要兼容,可忽略本说明。
79 | ### 对接方法
80 |
81 | 如果您想在其他服务内对接 miaospeed,可能没有现成的案例。但是,您依然可以参考如下思路:
82 |
83 | 1. miaospeed 对接本质是通过 ws 通道发送指令、传递信息。一般来说,您只需要连接 ws,构建请求结构体,签名请求,接收结果即可。
84 | 2. 连接 ws,这一步很简单,也就不用赘述了。(如果您在客户端强制断开链接,则任务会被自动中止)
85 | 3. 构建请求结构体,参考: https://github.com/AirportR/miaospeed/blob/fd7abecc2d36a0f18b08f048f9a53b7c0a26bd9e/interfaces/api_request.go#L50
86 | 4. 签名,参考: https://github.com/AirportR/miaospeed/blob/df6202409e87c5d944ab756608fd31d35390b5c0/utils/challenge.go#L39 其中需要传入两个参数。第一个参数是 `启动TOKEN` (即您启动 miaospeed 时传入的 -token 后的内容),第二个就是在第二步中您构建的结构体 `req`。签名的方法,通俗一些说明就是将结构体转换为 JSON String 然后与 `启动TOKEN` 和 `编译TOKEN` 切片分别累积做 SHA512 HASH。最后,将签名的字符串写入 `req.Challenge` 即可。
87 | 5. 发送完成签名后的请求,您就可以接收返回值了。服务器返回的结构体统一为 https://github.com/AirportR/miaospeed/blob/fd7abecc2d36a0f18b08f048f9a53b7c0a26bd9e/interfaces/api_response.go#L28
88 |
89 |
90 | ## 主要客户端实现
91 | 由于 miaospeed 的大部分功能体现为后端服务,因此需要自行实现客户端。可参考以下实现:
92 | * miaoko (闭源) 项目地址:无
93 | * koipy (初版开源,后续闭源) [项目地址](https://github.com/koipy-org/koipy) [项目文档](https://koipy.gitbook.io/koipy)
94 |
95 | 如未来有其他客户端实现,欢迎贡献。
96 | ## 版权与协议
97 |
98 | miaospeed 采用 AGPLv3 协议开源,您可以按照 AGPLv3 协议对 miaospeed 进行修改、贡献、分发、乃至商用。但请切记,您必须遵守 AGPLv3 协议下的一切义务,以免发生不必要的法律纠纷。
99 |
100 | ### 主要开源依赖公示
101 |
102 | miaospeed 采用了如下的开源项目:
103 |
104 | - Dreamacro/clash [GPLv3]
105 | - MetaCubeX/Clash.Meta [GPLv3]
106 | - juju/ratelimit [LGPLv3]
107 | - dop251/goja [MIT]
108 | - json-iterator/go [MIT]
109 | - pion/stun [MIT]
110 | - go-yaml/yaml [MIT]
111 | - gorilla/websocket [BSD]
112 | - jpillora/ipfilter [MIT]
113 |
114 | ## 抽象设计
115 |
116 | 如果您想贡献 miaospeed,您可以参考以下 miaospeed 的抽象设计:
117 |
118 | - **Matrix**: 数据矩阵 [interfaces/matrix.go]。即用户想要获取的某个数据的最小颗粒度。例如,用户希望了解某个节点的 RTT 延迟,则 TA 可以要求 miaospeed 对 `TEST_PING_RTT` [例如: service/matrices/httpping/matrix.go] 进行测试。
119 | - **Macro**: 运行时宏任务 [interfaces/macro.go]。如果用户希望批量运行数据矩阵,他们往往会做重复的事情。例如 `TEST_PING_RTT` 与 `TEST_PING_HTTP` 大多数时间都在做相同的事情。如果将两个 _Matrix_ 独立运行,则会浪费大量资源。因此,我们定义了 _Macro_ 最为一个最小颗粒度的执行体。由 _Macro_ 并行完成一系列耗时的操作,随后,_Matrix_ 将解析 _Macro_ 运行得到的数据,以填充自己的内容。
120 | - **Vendor**: 服务提供商 [interfaces/vendor.go]。miaospeed 本身只是一个测试工具,**它不具备任何代理能力**。因此,_Vendor_ 作为一个接口,为 miaospeed 提供了链接能力。
121 |
--------------------------------------------------------------------------------
/cli.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "path"
8 |
9 | "github.com/airportr/miaospeed/utils"
10 | )
11 |
12 | var cmdName = "miaospeed"
13 |
14 | type SubCliType string
15 |
16 | const (
17 | SCTMisc SubCliType = "misc"
18 | SCTServer SubCliType = "server"
19 | SCTScriptTest SubCliType = "script"
20 | )
21 |
22 | func RunCli() {
23 | subCmd := SubCliType("")
24 | if len(os.Args) >= 2 {
25 | subCmd = SubCliType(os.Args[1])
26 | }
27 |
28 | cmdName = path.Base(os.Args[0])
29 | switch subCmd {
30 | case SCTMisc:
31 | RunCliMisc()
32 | case SCTServer:
33 | RunCliServer()
34 | case SCTScriptTest:
35 | RunCliScriptTest()
36 | default:
37 | RunCliDefault()
38 | }
39 | }
40 |
41 | func RunCliDefault() {
42 | sflag := flag.NewFlagSet(cmdName, flag.ExitOnError)
43 |
44 | versionOnly := sflag.Bool("version", false, "display version and exit")
45 | sflag.Parse(os.Args[1:])
46 |
47 | if *versionOnly {
48 | fmt.Println(utils.LOGO)
49 | fmt.Printf("version: %s\n", utils.VERSION)
50 | fmt.Printf("commit: %s\n", utils.COMMIT)
51 | fmt.Printf("compilation time: %s\n", utils.COMPILATIONTIME)
52 | os.Exit(0)
53 | }
54 |
55 | sflag.Usage()
56 |
57 | fmt.Printf("\n")
58 | fmt.Printf("Subcommands of %s:\n", cmdName)
59 | fmt.Printf(" server\n")
60 | fmt.Printf(" start the miaospeed backend as a server.\n")
61 | fmt.Printf(" script\n")
62 | fmt.Printf(" run a temporary script test to test the correctness of your script.\n")
63 | fmt.Printf(" misc\n")
64 | fmt.Printf(" other utility toolkit provided by miaospeed.\n")
65 | fmt.Printf("Run this command to see the usage of the server option: \n")
66 | fmt.Printf(" %s server -help\n", cmdName)
67 | os.Exit(0)
68 | }
69 |
70 | func parseFlag(sflag *flag.FlagSet) {
71 | verboseMode := sflag.Bool("verbose", false, "whether to print out systems log")
72 |
73 | sflag.Parse(os.Args[2:])
74 |
75 | if *verboseMode {
76 | utils.VerboseLevel = utils.LTLog
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/cli_misc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "flag"
6 | "fmt"
7 | "os"
8 | "regexp"
9 |
10 | "github.com/airportr/miaospeed/utils"
11 | )
12 |
13 | const MAXMIND_DB_DOWNLOAD_URL = "https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz"
14 |
15 | var MAXMIND_EDITION_MATRIX = []string{
16 | "GeoLite2-ASN", "GeoLite2-City",
17 | }
18 |
19 | type MiscCliParams struct {
20 | MaxmindLicenseKey string
21 | }
22 |
23 | func InitConfigMisc() *MiscCliParams {
24 | stcp := &MiscCliParams{}
25 |
26 | sflag := flag.NewFlagSet(cmdName+" misc", flag.ExitOnError)
27 | sflag.StringVar(&stcp.MaxmindLicenseKey, "maxmind-update-license", "", "specify a maxmind license to update database.")
28 |
29 | parseFlag(sflag)
30 |
31 | return stcp
32 | }
33 |
34 | func RunCliMisc() {
35 | stcp := InitConfigMisc()
36 |
37 | if stcp.MaxmindLicenseKey != "" {
38 | // update maxmind database
39 | mmdbFilter := regexp.MustCompile(`\.mmdb$`)
40 | for _, edition := range MAXMIND_EDITION_MATRIX {
41 | url := fmt.Sprintf(MAXMIND_DB_DOWNLOAD_URL, edition, stcp.MaxmindLicenseKey)
42 | if downloadBytes, err := utils.DownloadBytes(url); err != nil {
43 | utils.DErrorf("Maxmind Updater | Cannot fetch content from server, edition=%s err=%s", edition, err.Error())
44 | } else if archiveEntries, err := utils.FindAndExtract(bytes.NewBuffer(downloadBytes), *mmdbFilter); err != nil || len(archiveEntries) == 0 {
45 | utils.DErrorf("Maxmind Updater | Cannot extract content from gzip file, edition=%s size=%d", edition, len(downloadBytes))
46 | } else {
47 | for file, fileBytes := range archiveEntries {
48 | if err := os.WriteFile(file, fileBytes, 0644); err != nil {
49 | utils.DErrorf("Maxmind Updater | Create local file, edition=%s size=%d file=%s err=%s", edition, len(fileBytes), file, err.Error())
50 | } else {
51 | utils.DWarnf("Maxmind Updater | File updated, edition=%s size=%d file=%s", edition, len(fileBytes), file)
52 | }
53 | }
54 | }
55 | }
56 | return
57 | }
58 |
59 | fmt.Printf("You have not specify any options, please call %s misc -help to see all available commands.\n", cmdName)
60 | }
61 |
--------------------------------------------------------------------------------
/cli_script.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/airportr/miaospeed/interfaces"
9 | "github.com/airportr/miaospeed/service/macros/script"
10 | "github.com/airportr/miaospeed/utils"
11 | "github.com/airportr/miaospeed/vendors"
12 | )
13 |
14 | type ScriptTestCliParams struct {
15 | ScriptName string
16 | }
17 |
18 | func InitConfigScriptTest() *ScriptTestCliParams {
19 | stcp := &ScriptTestCliParams{}
20 |
21 | sflag := flag.NewFlagSet(cmdName+" script", flag.ExitOnError)
22 | sflag.StringVar(&stcp.ScriptName, "file", "", "specify a script file to perform a test.")
23 |
24 | parseFlag(sflag)
25 |
26 | return stcp
27 | }
28 |
29 | func RunCliScriptTest() {
30 | stcp := InitConfigScriptTest()
31 |
32 | if stcp.ScriptName == "" {
33 | utils.DErrorf("Script Test | File name cannot be empty.")
34 | os.Exit(1)
35 | }
36 |
37 | fileContent, err := os.ReadFile(stcp.ScriptName)
38 | if err != nil {
39 | utils.DErrorf("Script Test | Cannot read the file, path=%s", stcp.ScriptName)
40 | os.Exit(1)
41 | }
42 |
43 | utils.VerboseLevel = utils.LTInfo
44 | utils.DWarnf("MiaoSpeed speedtesting client %s", utils.VERSION)
45 |
46 | vendor := vendors.Find(interfaces.VendorLocal)
47 | utils.DInfof("Script Test | Using vendor %s", vendor.Type())
48 | scriptResult := script.ExecScript(vendor, &interfaces.Script{
49 | Content: string(fileContent),
50 | })
51 |
52 | fmt.Println("\n" + utils.ToJSON(scriptResult))
53 | }
54 |
--------------------------------------------------------------------------------
/cli_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | "github.com/airportr/miaospeed/preconfigs"
10 | "github.com/airportr/miaospeed/service"
11 | "github.com/airportr/miaospeed/utils"
12 | )
13 |
14 | func InitConfigServer() *utils.GlobalConfig {
15 | gcfg := &utils.GCFG
16 | sflag := flag.NewFlagSet(cmdName+" server", flag.ExitOnError)
17 | sflag.StringVar(&gcfg.Token, "token", "", "specify the token used to sign request")
18 | sflag.StringVar(&gcfg.Binder, "bind", "", "bind a socket, can be format like 0.0.0.0:8080 or /tmp/unix_socket")
19 | sflag.UintVar(&gcfg.ConnTaskTreading, "connthread", 64, "parallel threads when processing normal connectivity tasks")
20 | sflag.UintVar(&gcfg.TaskLimit, "tasklimit", 1000, "limit of tasks in queue, default with 1000")
21 | sflag.Uint64Var(&gcfg.SpeedLimit, "speedlimit", 0, "speed ratelimit (in Bytes per Second), default with no limits")
22 | sflag.UintVar(&gcfg.PauseSecond, "pausesecond", 0, "pause such period after each speed job (seconds)")
23 | sflag.BoolVar(&gcfg.MiaoKoSignedTLS, "mtls", false, "enable miaoko certs for tls verification")
24 | sflag.BoolVar(&gcfg.NoSpeedFlag, "nospeed", false, "decline all speedtest requests")
25 | sflag.BoolVar(&gcfg.EnableIPv6, "ipv6", false, "enable ipv6 support")
26 | sflag.StringVar(&gcfg.MaxmindDB, "mmdb", "", "reroute all geoip query to local mmdbs. for example: test.mmdb,testcity.mmdb")
27 | path := sflag.String("path", "", "specific websocket path you want, default '/'")
28 | allowIP := sflag.String("allowip", "0.0.0.0/0,::/0", "allow ip range, can be format like 192.168.1.0/24,10.12.13.2")
29 | whiteList := sflag.String("whitelist", "", "bot id whitelist, can be format like 1111,2222,3333")
30 | pubKeyStr := sflag.String("serverpublickey", "", "specific the sever public key (PEM format)")
31 | privKeyStr := sflag.String("serverprivatekey", "", "specific the sever private key (PEM format)")
32 | parseFlag(sflag)
33 |
34 | gcfg.WhiteList = make([]string, 0)
35 | if *whiteList != "" {
36 | gcfg.WhiteList = strings.Split(*whiteList, ",")
37 | }
38 | if *allowIP != "" {
39 | if *allowIP == "0.0.0.0/0,::/0" {
40 | utils.DWarnf("MiaoSpeed Server | allow ip range is set to 0.0.0.0/0,::/0, which means any ip (full stack) can access this server, please use it with caution")
41 | }
42 | gcfg.AllowIPs = strings.Split(*allowIP, ",")
43 | }
44 | if *path != "" {
45 | if *path == "/" {
46 | gcfg.Path = "/"
47 | }
48 | gcfg.Path = "/" + strings.TrimPrefix(*path, "/")
49 | } else {
50 | // deprecated
51 | gcfg.Path = "/"
52 | }
53 | if gcfg.Path == "/" {
54 | utils.DWarnf("MiaoSpeed Server | using an unsafe websocket connection path: %s", gcfg.Path)
55 | } else {
56 | utils.DWarnf("MiaoSpeed Server | using a custom websocket connection path: %s", gcfg.Path)
57 | }
58 | if pubKey := utils.ReadFile(*pubKeyStr); pubKey != "" {
59 | utils.DLog("Override predefined tls certificates")
60 | preconfigs.MIAOKO_TLS_CRT = pubKey
61 | }
62 | if priKey := utils.ReadFile(*privKeyStr); priKey != "" {
63 | utils.DLog("Override predefined tls key")
64 | preconfigs.MIAOKO_TLS_KEY = priKey
65 | }
66 | return gcfg
67 | }
68 |
69 | func RunCliServer() {
70 | fmt.Println(utils.LOGO)
71 | InitConfigServer()
72 | utils.DWarnf("MiaoSpeed speedtesting client %s", utils.VERSION)
73 |
74 | // load maxmind db
75 | if utils.LoadMaxMindDB(utils.GCFG.MaxmindDB) != nil {
76 | os.Exit(1)
77 | }
78 |
79 | // start task server
80 | go service.StartTaskServer()
81 |
82 | // start api server
83 | service.CleanUpServer()
84 | go service.InitServer()
85 |
86 | <-utils.MakeSysChan()
87 |
88 | // clean up
89 | service.CleanUpServer()
90 | utils.DLog("shutting down.")
91 | }
92 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | ENV TZ=Asia/Shanghai
4 |
5 | COPY docker/install.sh /tmp
6 |
7 | RUN apk add --no-cache \
8 | curl && \
9 | ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
10 | echo $TZ > /etc/timezone && \
11 | chmod +x /tmp/install.sh && \
12 | sh /tmp/install.sh
13 |
14 | ENTRYPOINT ["/opt/miaospeed"]
--------------------------------------------------------------------------------
/docker/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # 获取系统架构信息
4 | ARCH=$(uname -m)
5 |
6 | echo "平台: ${ARCH}"
7 |
8 | # 获取系统位数
9 | BITS=$(getconf LONG_BIT)
10 |
11 | # 获取最新的标签名称
12 | LATEST_TAG=$(curl -s https://api.github.com/repos/AirportR/miaospeed/releases/latest | grep 'tag_name' | cut -d '"' -f 4)
13 |
14 | if [ "$ARCH" = "x86_64" ] && [ "$BITS" = "64" ]; then
15 | echo "架构: linux/amd64"
16 | ARCH="linux-amd64"
17 | elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
18 | echo "架构: linux/arm64"
19 | ARCH="linux-arm64"
20 | elif [ "$ARCH" = "armv7l" ]; then
21 | echo "架构: linux/arm/v7"
22 | ARCH="linux-armv7"
23 | elif [ "$ARCH" = "x86_64" ] && [ "$BITS" = "32" ]; then
24 | echo "架构: linux/386"
25 | ARCH="linux-386"
26 | fi
27 |
28 | curl -L "https://github.com/AirportR/miaospeed/releases/download/$LATEST_TAG/miaospeed-$ARCH-$LATEST_TAG.tar.gz" -o "/opt/miaospeed.tar.gz"
29 | tar -xzf /opt/miaospeed.tar.gz -C /opt/
30 | mv /opt/miaospeed-$ARCH /opt/miaospeed
31 | chmod +x /opt/miaospeed
--------------------------------------------------------------------------------
/engine/embeded.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import _ "embed"
4 |
5 | //go:embed embeded/predefined.js
6 | var PREDEFINED_SCRIPT string
7 |
8 | //go:embed embeded/default_geoip.js
9 | var DEFAULT_GEOIP_SCRIPT string
10 |
11 | //go:embed embeded/default_ip.js
12 | var DEFAULT_IP_SCRIPT string
13 |
--------------------------------------------------------------------------------
/engine/embeded/default_geoip.js:
--------------------------------------------------------------------------------
1 | const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36"
2 | function handler_ipleak(ip) {
3 | const isv6 = ip.includes(":")
4 | let geoip_api = `https://ipv4.ipleak.net/json/${ip}`
5 | if (isv6){
6 | geoip_api = `https://ipv4.ipleak.net/json/${ip}`
7 | }
8 | const content = fetch(geoip_api, {
9 | headers: {
10 | 'User-Agent': UA,
11 | },
12 | retry: 1,
13 | timeout: 3000,
14 | });
15 | const ret = safeParse(get(content, "body"));
16 | return {
17 | "ip": get(ret, "query", ""),
18 | "isp": get(ret, "isp_name", ""),
19 | "organization": get(ret, "isp_name", ""),
20 | "latitude": get(ret, "latitude", 0),
21 | "longitude": get(ret, "longitude", 0),
22 | "asn": parseInt(get(ret, "as_number", 0), 10) || 0,
23 | "asn_organization": get(ret, "isp_name", ""),
24 | "timezone": get(ret, "time_zone", ""),
25 | "region": get(ret, "region_name", ""),
26 | "city": get(ret, "city", ""),
27 | "country": get(ret, "city_name", ""),
28 | "country_code": get(ret, "country_code", ""),
29 | }
30 | }
31 |
32 | function handler(ip) {
33 | let result = {};
34 | result = handler_ipleak(ip)
35 | if (result && result.ip){
36 | return result;
37 | }
38 | return result;
39 | }
40 |
--------------------------------------------------------------------------------
/engine/embeded/default_ip.js:
--------------------------------------------------------------------------------
1 | function get_ip_by_cf() {
2 | const urls = ["https://1.1.1.1/cdn-cgi/trace", "https://[2606:4700:4700::1111]/cdn-cgi/trace"];
3 | const ipret = [];
4 | urls.forEach((url) => {
5 | const cf_content = (get(fetch(url, {
6 | headers: {
7 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36',
8 | },
9 | retry: 1,
10 | timeout: 3000,
11 | }), "body") || "").trim();
12 |
13 | const ip = (cf_content.match(/ip=(\S+)/)?.[1] || '').trim();
14 | if (ip) ipret.push(ip);
15 | });
16 | return ipret;
17 | }
18 | const ip_resolve_default = get_ip_by_cf;
19 |
--------------------------------------------------------------------------------
/engine/embeded/predefined.js:
--------------------------------------------------------------------------------
1 | function get(data, path, defaults=null) {
2 | var paths = path.split('.');
3 | for (var i = 0; i < paths.length; i++) {
4 | if (data === null || data === undefined) return defaults;
5 | data = data[paths[i]];
6 | }
7 | if (data === null || data === undefined) return defaults;
8 | return data;
9 | }
10 |
11 | function __json_stringify(data) {
12 | try {
13 | return JSON.stringify(data);
14 | } catch (err) { return ''; }
15 | }
16 |
17 | function __json_parse(data) {
18 | try {
19 | return JSON.parse(data);
20 | } catch (err) { return {}; }
21 | }
22 |
23 | const safeStringify = __json_stringify;
24 | const safeParse = __json_parse;
25 | const println = print;
26 |
--------------------------------------------------------------------------------
/engine/engine.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "time"
7 |
8 | "github.com/dop251/goja"
9 | "github.com/dop251/goja_nodejs/console"
10 | "github.com/dop251/goja_nodejs/require"
11 |
12 | "github.com/airportr/miaospeed/engine/factory"
13 | "github.com/airportr/miaospeed/interfaces"
14 | "github.com/airportr/miaospeed/utils"
15 | "github.com/airportr/miaospeed/utils/structs"
16 | )
17 |
18 | func VMNew() *goja.Runtime {
19 | rt := goja.New()
20 | new(require.Registry).Enable(rt)
21 | console.Enable(rt)
22 |
23 | rt.Set("print", factory.PrintFactory(rt, "Script Print |", utils.LTInfo))
24 | rt.Set("debug", factory.PrintFactory(rt, "Script Debug |", utils.LTLog))
25 |
26 | rt.SetMaxCallStackSize(1024)
27 | return rt
28 | }
29 |
30 | func VMNewWithVendor(p interfaces.Vendor, network interfaces.RequestOptionsNetwork) *goja.Runtime {
31 | vm := VMNew()
32 |
33 | if p != nil {
34 | pi := p.ProxyInfo()
35 | vm.Set("proxy", vm.ToValue(pi.Map()))
36 | } else {
37 | vm.Set("proxy", nil)
38 | }
39 |
40 | vm.Set("fetch", factory.FetchFactory(vm, p, network))
41 | vm.Set("netcat", factory.NetCatFactory(vm, p, network))
42 |
43 | return vm
44 | }
45 |
46 | func IsNotExtractError(err error) bool {
47 | if err != nil {
48 | return err.Error() == "cannot extract function from vm"
49 | }
50 | return false
51 | }
52 |
53 | func ThrowExecTaskErr(scenario string, err error) bool {
54 | if err != nil {
55 | if !IsNotExtractError(err) {
56 | utils.DErrorf("Engine Error | scenario=%s error=%s", scenario, err.Error())
57 | }
58 | return true
59 | }
60 | return false
61 | }
62 |
63 | func HasFunction(vm *goja.Runtime, caller string) bool {
64 | _, ok := goja.AssertFunction(vm.Get(caller))
65 | return ok
66 | }
67 |
68 | func ExecTaskCallback(vm *goja.Runtime, caller string, args ...interface{}) (ret goja.Value, err error) {
69 | utils.WrapError("Exec task callback error", func() error {
70 | if vm == nil {
71 | ret, err = goja.Undefined(), fmt.Errorf("vm is not initialized")
72 | return nil
73 | }
74 |
75 | fn, ok := goja.AssertFunction(vm.Get(caller))
76 | if !ok {
77 | ret, err = goja.Undefined(), fmt.Errorf("cannot extract function from vm")
78 | return nil
79 | }
80 |
81 | values := []goja.Value{}
82 | for _, arg := range args {
83 | values = append(values, vm.ToValue(arg))
84 | }
85 |
86 | ret, err = fn(goja.Undefined(), values...)
87 | return nil
88 | })
89 |
90 | return
91 | }
92 |
93 | func RunWithTimeout(vm *goja.Runtime, timeout time.Duration, fn func() (goja.Value, error)) (ret goja.Value, err error) {
94 | vmLock := sync.Mutex{}
95 | finished := false
96 |
97 | if timeout > 0 {
98 | timeout = structs.WithIn(timeout, time.Second, time.Minute)
99 | time.AfterFunc(timeout, func() {
100 | vmLock.Lock()
101 | defer vmLock.Unlock()
102 |
103 | if !finished {
104 | finished = true
105 | vm.Interrupt("script executing too long")
106 | }
107 | })
108 |
109 | }
110 |
111 | ret, err = fn()
112 |
113 | vmLock.Lock()
114 | finished = true
115 | vmLock.Unlock()
116 |
117 | return
118 | }
119 |
--------------------------------------------------------------------------------
/engine/factory/fetch.go:
--------------------------------------------------------------------------------
1 | package factory
2 |
3 | import (
4 | "github.com/airportr/miaospeed/engine/helpers"
5 | "github.com/airportr/miaospeed/interfaces"
6 | "github.com/airportr/miaospeed/vendors"
7 | "github.com/dop251/goja"
8 | )
9 |
10 | func FetchFactory(vm *goja.Runtime, p interfaces.Vendor, network interfaces.RequestOptionsNetwork) func(call goja.FunctionCall) goja.Value {
11 | return func(call goja.FunctionCall) goja.Value {
12 | url, _ := helpers.VMSafeStr(call.Argument(0))
13 | params, _ := helpers.VMSafeObj(vm, call.Argument(1))
14 |
15 | method := "GET"
16 | body := ""
17 | useHost := false
18 | noRedir := false
19 | retry := 0
20 | timeout := int64(3000)
21 | headers := map[string]string{}
22 | cookies := map[string]string{}
23 |
24 | if params != nil {
25 | if v, ok := helpers.VMSafeStr(params.Get("method")); ok {
26 | method = v
27 | }
28 | if v, ok := helpers.VMSafeStr(params.Get("body")); ok {
29 | body = v
30 | }
31 | if v, ok := helpers.VMSafeBool(params.Get("useHost")); ok {
32 | useHost = v
33 | }
34 | if v, ok := helpers.VMSafeBool(params.Get("noRedir")); ok {
35 | noRedir = v
36 | }
37 | if v, ok := helpers.VMSafeInt64(params.Get("retry")); ok {
38 | retry = int(v)
39 | }
40 | if v, ok := helpers.VMSafeInt64(params.Get("timeout")); ok {
41 | timeout = v
42 | }
43 | if vo, _ := helpers.VMSafeObj(vm, params.Get("headers")); vo != nil {
44 | for _, key := range vo.Keys() {
45 | if vv, ok := helpers.VMSafeStr(vo.Get(key)); ok {
46 | headers[key] = vv
47 | }
48 | }
49 | }
50 | if vo, _ := helpers.VMSafeObj(vm, params.Get("cookies")); vo != nil {
51 | for _, key := range vo.Keys() {
52 | if vv, ok := helpers.VMSafeStr(vo.Get(key)); ok {
53 | cookies[key] = vv
54 | }
55 | }
56 | }
57 | }
58 |
59 | if useHost {
60 | p = nil
61 | }
62 | retBody, resp, redirs := vendors.RequestWithRetry(p, retry, timeout, &interfaces.RequestOptions{
63 | Method: method,
64 | URL: url,
65 | Headers: headers,
66 | Cookies: cookies,
67 | Body: []byte(body),
68 | NoRedir: noRedir,
69 | Network: network,
70 | })
71 |
72 | var retMap map[string]interface{} = nil
73 | if resp != nil {
74 | retMap = make(map[string]interface{})
75 | retMap["status"] = resp.Status
76 | retMap["statusCode"] = resp.StatusCode
77 | retMap["cookies"] = resp.Cookies()
78 | retMap["headers"] = resp.Header
79 | retMap["method"] = method
80 | retMap["url"] = url
81 | retMap["body"] = string(retBody)
82 | retMap["redirects"] = redirs
83 | }
84 |
85 | if retMap == nil {
86 | return goja.Null()
87 | } else {
88 | return vm.ToValue(retMap)
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/engine/factory/netcat.go:
--------------------------------------------------------------------------------
1 | package factory
2 |
3 | import (
4 | "github.com/airportr/miaospeed/engine/helpers"
5 | "github.com/airportr/miaospeed/interfaces"
6 | "github.com/airportr/miaospeed/vendors"
7 | "github.com/dop251/goja"
8 | )
9 |
10 | func NetCatFactory(vm *goja.Runtime, p interfaces.Vendor, network interfaces.RequestOptionsNetwork) func(call goja.FunctionCall) goja.Value {
11 | return func(call goja.FunctionCall) goja.Value {
12 | addr, _ := helpers.VMSafeStr(call.Argument(0))
13 | data, _ := helpers.VMSafeStr(call.Argument(1))
14 | params, _ := helpers.VMSafeObj(vm, call.Argument(2))
15 |
16 | retry := 0
17 | useHost := false
18 | timeout := int64(3000)
19 |
20 | if params != nil {
21 | if v, ok := helpers.VMSafeBool(params.Get("useHost")); ok {
22 | useHost = v
23 | }
24 | if v, ok := helpers.VMSafeInt64(params.Get("timeout")); ok {
25 | timeout = v
26 | }
27 | if v, ok := helpers.VMSafeInt64(params.Get("retry")); ok {
28 | retry = int(v)
29 | }
30 | }
31 |
32 | if useHost {
33 | p = nil
34 | }
35 |
36 | returns, err := vendors.NetCatWithRetry(p, retry, timeout, addr, []byte(data), network)
37 |
38 | retMap := map[string]string{
39 | "error": "",
40 | "data": string(returns),
41 | }
42 | if err != nil {
43 | retMap["error"] = err.Error()
44 | }
45 |
46 | return vm.ToValue(retMap)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/engine/factory/print.go:
--------------------------------------------------------------------------------
1 | package factory
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/airportr/miaospeed/utils"
7 | "github.com/dop251/goja"
8 | )
9 |
10 | func PrintFactory(vm *goja.Runtime, prefix string, logType utils.LogType) func(call goja.FunctionCall) goja.Value {
11 | return func(call goja.FunctionCall) goja.Value {
12 | prep := prefix
13 | args := call.Arguments
14 | pass := make([]interface{}, len(args))
15 | for i := 0; i < len(args); i++ {
16 | prep += " %v"
17 | pass[i] = args[i]
18 | }
19 | prep = fmt.Sprintf(prep, pass...)
20 | utils.DBase(logType, prep)
21 | return vm.ToValue(prep)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/engine/helpers/helpers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | "github.com/dop251/goja"
8 | jsoniter "github.com/json-iterator/go"
9 | )
10 |
11 | func VMCheck(v goja.Value) bool {
12 | return v != nil && !goja.IsNull(v) && !goja.IsUndefined(v)
13 | }
14 |
15 | func VMSafeStr(v goja.Value) (string, bool) {
16 | if VMCheck(v) && v.ExportType().Kind() == reflect.String {
17 | return v.Export().(string), true
18 | }
19 | return "", false
20 | }
21 |
22 | func VMSafeBool(v goja.Value) (bool, bool) {
23 | if VMCheck(v) && v.ExportType().Kind() == reflect.Bool {
24 | return v.Export().(bool), true
25 | }
26 | return false, false
27 | }
28 |
29 | func VMSafeInt64(v goja.Value) (int64, bool) {
30 | if VMCheck(v) && v.ExportType().Kind() == reflect.Int64 {
31 | return v.Export().(int64), true
32 | }
33 | return 0, false
34 | }
35 |
36 | func VMSafeObj(vm *goja.Runtime, v goja.Value) (*goja.Object, bool) {
37 | if VMCheck(v) && v.ExportType().Kind() == reflect.Map {
38 | vo := v.ToObject(vm)
39 | if vo != nil {
40 | return vo, true
41 | }
42 | }
43 | return nil, false
44 | }
45 |
46 | func VMSafeMarshal(target interface{}, obj goja.Value, vm *goja.Runtime) error {
47 | if fn, ok := goja.AssertFunction(vm.Get("__json_stringify")); ok {
48 | ret, _ := fn(goja.Undefined(), obj)
49 | if v, ok := VMSafeStr(ret); ok {
50 | if v == "" {
51 | return fmt.Errorf("cannot marshal an empty string")
52 | }
53 | return jsoniter.UnmarshalFromString(v, target)
54 | }
55 | return fmt.Errorf("cannot read from stringify function")
56 | }
57 | return fmt.Errorf("cannot find stringify function")
58 | }
59 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/airportr/miaospeed
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
7 | github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
8 | github.com/gofrs/uuid v4.4.0+incompatible
9 | github.com/gorilla/websocket v1.5.3
10 | github.com/json-iterator/go v1.1.12
11 | github.com/juju/ratelimit v1.0.2
12 | github.com/metacubex/mihomo v1.19.3
13 | github.com/oschwald/maxminddb-golang v1.13.1
14 | github.com/pion/stun v0.6.1
15 | github.com/sirupsen/logrus v1.9.3
16 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
17 | golang.org/x/sync v0.11.0
18 | gopkg.in/yaml.v2 v2.4.0
19 | )
20 |
21 | require (
22 | github.com/3andne/restls-client-go v0.1.6 // indirect
23 | github.com/RyuaNerin/go-krypto v1.2.4 // indirect
24 | github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
25 | github.com/andybalholm/brotli v1.0.6 // indirect
26 | github.com/bahlo/generic-list-go v0.2.0 // indirect
27 | github.com/cloudflare/circl v1.3.7 // indirect
28 | github.com/coreos/go-iptables v0.8.0 // indirect
29 | github.com/dlclark/regexp2 v1.11.5 // indirect
30 | github.com/ebitengine/purego v0.8.2 // indirect
31 | github.com/enfein/mieru/v3 v3.11.2 // indirect
32 | github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
33 | github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
34 | github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
35 | github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
36 | github.com/gaukas/godicttls v0.0.4 // indirect
37 | github.com/go-ole/go-ole v1.3.0 // indirect
38 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
39 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
40 | github.com/gobwas/httphead v0.1.0 // indirect
41 | github.com/gobwas/pool v0.2.1 // indirect
42 | github.com/gobwas/ws v1.4.0 // indirect
43 | github.com/gofrs/uuid/v5 v5.3.1 // indirect
44 | github.com/google/btree v1.1.3 // indirect
45 | github.com/google/go-cmp v0.6.0 // indirect
46 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
47 | github.com/hashicorp/yamux v0.1.2 // indirect
48 | github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
49 | github.com/josharian/native v1.1.0 // indirect
50 | github.com/klauspost/compress v1.17.9 // indirect
51 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect
52 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
53 | github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
54 | github.com/mdlayher/netlink v1.7.2 // indirect
55 | github.com/mdlayher/socket v0.4.1 // indirect
56 | github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
57 | github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
58 | github.com/metacubex/chacha v0.1.1 // indirect
59 | github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
60 | github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
61 | github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
62 | github.com/metacubex/randv2 v0.2.0 // indirect
63 | github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
64 | github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
65 | github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
66 | github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
67 | github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
68 | github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
69 | github.com/metacubex/utls v1.6.6 // indirect
70 | github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
71 | github.com/miekg/dns v1.1.63 // indirect
72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
73 | github.com/modern-go/reflect2 v1.0.2 // indirect
74 | github.com/mroth/weightedrand/v2 v2.1.0 // indirect
75 | github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
76 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect
77 | github.com/openacid/low v0.1.21 // indirect
78 | github.com/pierrec/lz4/v4 v4.1.14 // indirect
79 | github.com/pion/dtls/v2 v2.2.7 // indirect
80 | github.com/pion/logging v0.2.2 // indirect
81 | github.com/pion/transport/v2 v2.2.1 // indirect
82 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
83 | github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
84 | github.com/quic-go/qpack v0.4.0 // indirect
85 | github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
86 | github.com/sagernet/sing v0.5.2 // indirect
87 | github.com/sagernet/sing-mux v0.2.1 // indirect
88 | github.com/sagernet/sing-shadowtls v0.1.5 // indirect
89 | github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
90 | github.com/samber/lo v1.49.1 // indirect
91 | github.com/shirou/gopsutil/v4 v4.25.1 // indirect
92 | github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
93 | github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
94 | github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
95 | github.com/tklauser/go-sysconf v0.3.12 // indirect
96 | github.com/tklauser/numcpus v0.6.1 // indirect
97 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
98 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
99 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
100 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
101 | gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
102 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
103 | go.uber.org/mock v0.4.0 // indirect
104 | golang.org/x/crypto v0.33.0 // indirect
105 | golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
106 | golang.org/x/mod v0.20.0 // indirect
107 | golang.org/x/net v0.35.0 // indirect
108 | golang.org/x/sys v0.30.0 // indirect
109 | golang.org/x/text v0.22.0 // indirect
110 | golang.org/x/time v0.7.0 // indirect
111 | golang.org/x/tools v0.24.0 // indirect
112 | google.golang.org/protobuf v1.34.2 // indirect
113 | lukechampine.com/blake3 v1.3.0 // indirect
114 | )
115 |
116 | replace github.com/miaokobot/miaospeed v0.0.0-20230313132009-0234a01d9daa => github.com/airportr/miaospeed v0.0.2
117 |
--------------------------------------------------------------------------------
/interfaces/api_request.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type SlaveRequestMatrixEntry struct {
4 | Type SlaveRequestMatrixType
5 | Params string
6 | }
7 |
8 | type SlaveRequestOptions struct {
9 | Filter string
10 | Matrices []SlaveRequestMatrixEntry
11 | }
12 |
13 | func (sro *SlaveRequestOptions) Clone() *SlaveRequestOptions {
14 | return &SlaveRequestOptions{
15 | Filter: sro.Filter,
16 | Matrices: cloneSlice(sro.Matrices),
17 | }
18 | }
19 |
20 | type SlaveRequestBasics struct {
21 | ID string
22 | Slave string
23 | SlaveName string
24 | Invoker string
25 | Version string
26 | }
27 |
28 | func (srb *SlaveRequestBasics) Clone() *SlaveRequestBasics {
29 | return &SlaveRequestBasics{
30 | ID: srb.ID,
31 | Slave: srb.Slave,
32 | SlaveName: srb.SlaveName,
33 | Invoker: srb.Invoker,
34 | Version: srb.Version,
35 | }
36 | }
37 |
38 | type SlaveRequestNode struct {
39 | Name string
40 | Payload string
41 | }
42 |
43 | func (srn *SlaveRequestNode) Clone() *SlaveRequestNode {
44 | return &SlaveRequestNode{
45 | Name: srn.Name,
46 | Payload: srn.Payload,
47 | }
48 | }
49 |
50 | type SlaveRequestV1 struct {
51 | Basics SlaveRequestBasics
52 | Options SlaveRequestOptions
53 | Configs SlaveRequestConfigsV1
54 |
55 | Vendor VendorType
56 | Nodes []SlaveRequestNode
57 |
58 | RandomSequence string
59 | Challenge string
60 | }
61 |
62 | func (sr *SlaveRequestV1) Clone() *SlaveRequestV1 {
63 | return &SlaveRequestV1{
64 | Basics: *sr.Basics.Clone(),
65 | Options: *sr.Options.Clone(),
66 | Configs: *sr.Configs.Clone(),
67 | Nodes: cloneSlice(sr.Nodes),
68 | RandomSequence: sr.RandomSequence,
69 | Challenge: sr.Challenge,
70 | }
71 | }
72 |
73 | type SlaveRequest struct {
74 | Basics SlaveRequestBasics
75 | Options SlaveRequestOptions
76 | Configs SlaveRequestConfigsV2
77 |
78 | Vendor VendorType
79 | Nodes []SlaveRequestNode
80 |
81 | RandomSequence string
82 | Challenge string
83 | }
84 |
85 | func (sr *SlaveRequest) Clone() *SlaveRequest {
86 | return &SlaveRequest{
87 | Basics: *sr.Basics.Clone(),
88 | Options: *sr.Options.Clone(),
89 | Configs: *sr.Configs.Clone(),
90 | Nodes: cloneSlice(sr.Nodes),
91 | RandomSequence: sr.RandomSequence,
92 | Challenge: sr.Challenge,
93 | }
94 | }
95 |
96 | func (sr *SlaveRequest) CloneToV1() *SlaveRequestV1 {
97 | return &SlaveRequestV1{
98 | Basics: *sr.Basics.Clone(),
99 | Options: *sr.Options.Clone(),
100 | Configs: *sr.Configs.CloneToV1(),
101 | Nodes: cloneSlice(sr.Nodes),
102 | RandomSequence: sr.RandomSequence,
103 | Challenge: sr.Challenge,
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/interfaces/api_request_config.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | import (
4 | "github.com/airportr/miaospeed/preconfigs"
5 | "github.com/airportr/miaospeed/utils/structs"
6 | )
7 |
8 | type SlaveRequestConfigsV1 struct {
9 | STUNURL string `yaml:"stunURL,omitempty" cf:"name=🫙 STUN 地址"`
10 | DownloadURL string `yaml:"downloadURL,omitempty" cf:"name=📃 测速文件"`
11 | DownloadDuration int64 `yaml:"downloadDuration,omitempty" cf:"name=⏱️ 测速时长 (单位: 秒)"`
12 | DownloadThreading uint `yaml:"downloadThreading,omitempty" cf:"name=🧶 测速线程数"`
13 |
14 | PingAverageOver uint16 `yaml:"pingAverageOver,omitempty" cf:"name=🧮 多次 Ping 求均值,value"`
15 | PingAddress string `yaml:"pingAddress,omitempty" cf:"name=🏫 URL Ping 地址"`
16 |
17 | TaskRetry uint `yaml:"taskRetry,omitempty" cf:"name=🐛 测试重试次数"`
18 | DNSServers []string `yaml:"dnsServers,omitempty" cf:"name=💾 自定义DNS服务器,childvalue"`
19 |
20 | TaskTimeout uint `yaml:"-" fw:"readonly"`
21 | Scripts []Script `yaml:"-" fw:"readonly"`
22 | }
23 |
24 | const (
25 | ApiV0 = iota
26 | ApiV1 = iota
27 | //ApiV2 = iota
28 | )
29 |
30 | type SlaveRequestConfigsV2 struct {
31 | *SlaveRequestConfigsV1
32 | ApiVersion int `yaml:"apiVersion,omitempty" cf:"name=🧬API版本,用于兼容Miaoko以及其他客户端"`
33 | }
34 |
35 | func (srcv2 *SlaveRequestConfigsV2) Clone() *SlaveRequestConfigsV2 {
36 | return &SlaveRequestConfigsV2{
37 | SlaveRequestConfigsV1: srcv2.SlaveRequestConfigsV1.Clone(),
38 | ApiVersion: srcv2.ApiVersion,
39 | }
40 | }
41 |
42 | func (srcv2 *SlaveRequestConfigsV2) CloneToV1() *SlaveRequestConfigsV1 {
43 | return &SlaveRequestConfigsV1{
44 | STUNURL: srcv2.STUNURL,
45 | DownloadURL: srcv2.DownloadURL,
46 | DownloadDuration: srcv2.DownloadDuration,
47 | DownloadThreading: srcv2.DownloadThreading,
48 |
49 | PingAverageOver: srcv2.PingAverageOver,
50 | PingAddress: srcv2.PingAddress,
51 |
52 | TaskRetry: srcv2.TaskRetry,
53 | DNSServers: cloneSlice(srcv2.DNSServers),
54 |
55 | TaskTimeout: srcv2.TaskTimeout,
56 | Scripts: srcv2.Scripts,
57 | }
58 | }
59 |
60 | func (src *SlaveRequestConfigsV1) DescriptionText() string {
61 | hint := structs.X("案例:\ndownloadDuration: 取值范围 [1,30]\ndownloadThreading: 取值范围 [1,8]\ntaskThreading: 取值范围 [1,32]\ntaskRetry: 取值范围 [1,10]\n\n当前:\n")
62 | cont := "empty"
63 | if src != nil {
64 | cont = structs.X("downloadDuration: %d\ndownloadThreading: %d\ntaskRetry: %d\n", src.DownloadDuration, src.DownloadThreading, src.TaskRetry)
65 | }
66 | return hint + cont
67 | }
68 |
69 | func (src *SlaveRequestConfigsV1) Clone() *SlaveRequestConfigsV1 {
70 | return &SlaveRequestConfigsV1{
71 | STUNURL: src.STUNURL,
72 | DownloadURL: src.DownloadURL,
73 | DownloadDuration: src.DownloadDuration,
74 | DownloadThreading: src.DownloadThreading,
75 |
76 | PingAverageOver: src.PingAverageOver,
77 | PingAddress: src.PingAddress,
78 |
79 | TaskRetry: src.TaskRetry,
80 | DNSServers: cloneSlice(src.DNSServers),
81 |
82 | TaskTimeout: src.TaskTimeout,
83 | Scripts: src.Scripts,
84 | }
85 | }
86 |
87 | func (src *SlaveRequestConfigsV1) Merge(from *SlaveRequestConfigsV1) *SlaveRequestConfigsV1 {
88 | ret := src.Clone()
89 | if from.STUNURL != "" {
90 | ret.STUNURL = from.STUNURL
91 | }
92 |
93 | if from.DownloadURL != "" {
94 | ret.DownloadURL = from.DownloadURL
95 | }
96 | if from.DownloadDuration != 0 {
97 | ret.DownloadDuration = from.DownloadDuration
98 | }
99 | if from.DownloadThreading != 0 {
100 | ret.DownloadThreading = from.DownloadThreading
101 | }
102 |
103 | if from.PingAverageOver != 0 {
104 | ret.PingAverageOver = from.PingAverageOver
105 | }
106 | if from.PingAddress != "" {
107 | ret.PingAddress = from.PingAddress
108 | }
109 |
110 | if from.TaskRetry != 0 {
111 | ret.TaskRetry = from.TaskRetry
112 | }
113 |
114 | if from.DNSServers != nil {
115 | ret.DNSServers = from.DNSServers[:]
116 | }
117 |
118 | if from.TaskTimeout != 0 {
119 | ret.TaskTimeout = from.TaskTimeout
120 | }
121 | if from.Scripts != nil {
122 | ret.Scripts = from.Scripts
123 | }
124 |
125 | return ret
126 | }
127 |
128 | func (src *SlaveRequestConfigsV1) Check() *SlaveRequestConfigsV1 {
129 | if src == nil {
130 | src = &SlaveRequestConfigsV1{}
131 | }
132 |
133 | if src.STUNURL == "" {
134 | src.STUNURL = preconfigs.PROXY_DEFAULT_STUN_SERVER
135 | }
136 | if src.DownloadURL == "" {
137 | src.DownloadURL = preconfigs.SPEED_DEFAULT_LARGE_FILE_DEFAULT
138 | }
139 | if src.DownloadDuration < 1 || src.DownloadDuration > 30 {
140 | src.DownloadDuration = preconfigs.SPEED_DEFAULT_DURATION
141 | }
142 | if src.DownloadThreading < 1 || src.DownloadThreading > 32 {
143 | src.DownloadThreading = preconfigs.SPEED_DEFAULT_THREADING
144 | }
145 |
146 | if src.TaskRetry < 1 || src.TaskRetry > 10 {
147 | src.TaskRetry = preconfigs.SLAVE_DEFAULT_RETRY
148 | }
149 |
150 | if src.PingAddress == "" {
151 | src.PingAddress = preconfigs.SLAVE_DEFAULT_PING
152 | }
153 | if src.PingAverageOver == 0 || src.PingAverageOver > 16 {
154 | src.PingAverageOver = 1
155 | }
156 |
157 | if src.DNSServers == nil {
158 | src.DNSServers = make([]string, 0)
159 | }
160 |
161 | if src.TaskTimeout < 10 || src.TaskTimeout > 10000 {
162 | src.TaskTimeout = preconfigs.SLAVE_DEFAULT_TIMEOUT
163 | }
164 | if src.Scripts == nil {
165 | src.Scripts = make([]Script, 0)
166 | }
167 |
168 | return src
169 | }
170 |
--------------------------------------------------------------------------------
/interfaces/api_response.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type SlaveEntrySlot struct {
4 | Grouping string
5 | ProxyInfo ProxyInfo
6 | InvokeDuration int64
7 | Matrices []MatrixResponse
8 | }
9 |
10 | func (ses *SlaveEntrySlot) Get(idx int) *MatrixResponse {
11 | if idx < len(ses.Matrices) {
12 | return &ses.Matrices[idx]
13 | }
14 | return nil
15 | }
16 |
17 | type SlaveTask struct {
18 | Request SlaveRequest
19 | Results []SlaveEntrySlot
20 | }
21 |
22 | type SlaveProgress struct {
23 | Index int
24 | Record SlaveEntrySlot
25 | Queuing int
26 | }
27 |
28 | type SlaveResponse struct {
29 | ID string
30 | MiaoSpeedVersion string
31 |
32 | Error string
33 | Result *SlaveTask
34 | Progress *SlaveProgress
35 | }
36 |
--------------------------------------------------------------------------------
/interfaces/geoip.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | import (
4 | "sort"
5 | "strings"
6 | )
7 |
8 | type IPStacks struct {
9 | IPv4 []string
10 | IPv6 []string
11 | }
12 |
13 | func (ips *IPStacks) Init() *IPStacks {
14 | if ips == nil {
15 | ips = &IPStacks{}
16 | }
17 | ips.IPv4 = []string{}
18 | ips.IPv6 = []string{}
19 | return ips
20 | }
21 |
22 | func (ips *IPStacks) Count() int {
23 | if ips == nil {
24 | return 0
25 | }
26 | return len(ips.IPv4) + len(ips.IPv6)
27 | }
28 |
29 | type GeoInfo struct {
30 | Org string `json:"organization"`
31 | Lon float32 `json:"longitude"`
32 | Lat float32 `json:"latitude"`
33 | TimeZone string `json:"timezone"`
34 | ISP string `json:"isp"`
35 | ASN int `json:"asn"`
36 | ASNOrg string `json:"asn_organization"`
37 | Country string `json:"country"`
38 | IP string `json:"ip"`
39 | ContinentCode string `json:"continent_code"`
40 | CountryCode string `json:"country_code"`
41 |
42 | StackType string `json:"stackType"`
43 | }
44 |
45 | func (gi *GeoInfo) IsV6() bool {
46 | return gi != nil && gi.IP != "" && strings.Contains(gi.IP, ":")
47 | }
48 |
49 | type MultiStacks struct {
50 | Domain string // 域组,作为 In 时为域名,Out 时则为线路本身
51 | MainStack *GeoInfo // deprecating
52 | IPv4Stack []*GeoInfo
53 | IPv6Stack []*GeoInfo
54 | }
55 |
56 | func (tms *MultiStacks) Repr() string {
57 | repr := []string{}
58 | if tms == nil || tms.Count() == 0 {
59 | return ""
60 | }
61 | for _, v4 := range tms.IPv4Stack {
62 | repr = append(repr, v4.IP)
63 | }
64 | for _, v6 := range tms.IPv6Stack {
65 | repr = append(repr, v6.IP)
66 | }
67 |
68 | sort.Strings(repr)
69 | return strings.Join(repr, ",")
70 | }
71 |
72 | func (tms *MultiStacks) FirstV2(tag string) *GeoInfo {
73 | if tms == nil || tms.Count() == 0 {
74 | return nil
75 | }
76 |
77 | // check tags
78 | if tag == "" {
79 | tag = "46"
80 | }
81 | if len(tag) > 2 {
82 | return nil
83 | } else if len(tag) == 2 && tag[0] == tag[1] {
84 | return nil
85 | } else {
86 | for _, r := range tag {
87 | if r != '4' && r != '6' {
88 | return nil
89 | }
90 | }
91 | }
92 |
93 | // get ordered by the sequence order of tags
94 | stacks := []*GeoInfo{}
95 | for _, r := range tag {
96 | if r == '4' && len(tms.IPv4Stack) > 0 {
97 | stacks = append(stacks, tms.IPv4Stack...)
98 | } else if r == '6' && len(tms.IPv6Stack) > 0 {
99 | stacks = append(stacks, tms.IPv6Stack...)
100 | }
101 | }
102 |
103 | for _, ip := range stacks {
104 | if ip.IP != "" {
105 | return ip
106 | }
107 | }
108 | return nil
109 | }
110 |
111 | func (tms *MultiStacks) First(tag string) *GeoInfo {
112 | if tms == nil || tms.Count() == 0 {
113 | return nil
114 | }
115 |
116 | if tag != "v6" {
117 | for _, v4 := range tms.IPv4Stack {
118 | if v4.IP != "" {
119 | return v4
120 | }
121 | }
122 | }
123 |
124 | if tag != "v4" {
125 | for _, v6 := range tms.IPv6Stack {
126 | if v6.IP != "" {
127 | return v6
128 | }
129 | }
130 | }
131 |
132 | return nil
133 | }
134 |
135 | func (tms *MultiStacks) ForEach(assignedMain *GeoInfo) map[int][]*GeoInfo {
136 | result := make(map[int][]*GeoInfo)
137 | if assignedMain != nil && (tms == nil || tms.Count() == 0) {
138 | result[assignedMain.ASN] = []*GeoInfo{assignedMain}
139 | return result
140 | }
141 | if tms == nil {
142 | return result
143 | }
144 | for _, v4 := range tms.IPv4Stack {
145 | result[v4.ASN] = append(result[v4.ASN], v4)
146 | }
147 | for _, v6 := range tms.IPv6Stack {
148 | result[v6.ASN] = append(result[v6.ASN], v6)
149 | }
150 |
151 | return result
152 | }
153 |
154 | func (tms *MultiStacks) Count() int {
155 | if tms == nil {
156 | return 0
157 | }
158 | a, b := tms.V46StackCount()
159 | return a + b
160 | }
161 |
162 | func (tms *MultiStacks) V46StackCount() (int, int) {
163 | if tms == nil {
164 | return 0, 0
165 | }
166 | return len(tms.IPv4Stack), len(tms.IPv6Stack)
167 | }
168 |
169 | func (tms *MultiStacks) V46StackInfo() string {
170 | v4, v6 := tms.V46StackCount()
171 | ret := "N/A"
172 | if v4 > 0 {
173 | ret = "4⃣"
174 | }
175 | if v6 > 0 {
176 | ret = "6⃣"
177 | }
178 | if v4 > 0 && v6 > 0 {
179 | ret = "4⃣6⃣"
180 | }
181 | return ret
182 | }
183 |
--------------------------------------------------------------------------------
/interfaces/macro.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type SlaveRequestMacroType string
4 |
5 | const (
6 | MacroSpeed SlaveRequestMacroType = "SPEED"
7 |
8 | MacroPing SlaveRequestMacroType = "PING"
9 | MacroUDP SlaveRequestMacroType = "UDP"
10 | MacroScript SlaveRequestMacroType = "SCRIPT"
11 | MacroGeo SlaveRequestMacroType = "GEO"
12 | MacroSleep SlaveRequestMacroType = "SLEEP"
13 | MacroInvalid SlaveRequestMacroType = "INVALID"
14 | )
15 |
16 | // Macro is the atom runner for a job. Since some matrices
17 | // could be combined, e.g. HTTPPing / RTTPing, so instead of
18 | // triggering two similar jobs, we only run a macro job once
19 | // and return attributes for multiple matrices
20 | type SlaveRequestMacro interface {
21 | // define the macro type to match
22 | Type() SlaveRequestMacroType
23 |
24 | // define the task for the macro,
25 | Run(proxy Vendor, request *SlaveRequest) error
26 | }
27 |
--------------------------------------------------------------------------------
/interfaces/macro_fields.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type MacroFieldType string
4 |
5 | const (
6 | MFTPingRTT = "PingRTT"
7 | MFTPingRequest = "PingRequest"
8 |
9 | MFTNATType = "NATType"
10 | )
11 |
--------------------------------------------------------------------------------
/interfaces/matrix.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type SlaveRequestMatrixType string
4 |
5 | const (
6 | MatrixAverageSpeed SlaveRequestMatrixType = "SPEED_AVERAGE"
7 | MatrixMaxSpeed SlaveRequestMatrixType = "SPEED_MAX"
8 | MatrixPerSecondSpeed SlaveRequestMatrixType = "SPEED_PER_SECOND"
9 |
10 | MatrixUDPType SlaveRequestMatrixType = "UDP_TYPE"
11 |
12 | MatrixInboundGeoIP SlaveRequestMatrixType = "GEOIP_INBOUND"
13 | MatrixOutboundGeoIP SlaveRequestMatrixType = "GEOIP_OUTBOUND"
14 |
15 | MatrixScriptTest SlaveRequestMatrixType = "TEST_SCRIPT"
16 | MatrixHTTPPing SlaveRequestMatrixType = "TEST_PING_CONN"
17 | MatrixRTTPing SlaveRequestMatrixType = "TEST_PING_RTT"
18 | MatrixMAXHTTPPing SlaveRequestMatrixType = "TEST_PING_MAX_CONN"
19 | MatrixMAXRTTPing SlaveRequestMatrixType = "TEST_PING_MAX_RTT"
20 | MatrixTotalHTTPPing SlaveRequestMatrixType = "TEST_PING_TOTAL_CONN"
21 | MatrixTotalRTTPing SlaveRequestMatrixType = "TEST_PING_TOTAL_RTT"
22 | MatrixSDRTT SlaveRequestMatrixType = "TEST_PING_SD_RTT"
23 | MatrixSDHTTP SlaveRequestMatrixType = "TEST_PING_SD_CONN"
24 | MatrixHTTPCode SlaveRequestMatrixType = "TEST_HTTP_CODE"
25 | MatrixPacketLoss SlaveRequestMatrixType = "TEST_PING_PACKET_LOSS"
26 | MatrixSleep SlaveRequestMatrixType = "DEBUG_SLEEP"
27 | MatrixInvalid SlaveRequestMatrixType = "INVALID"
28 | )
29 |
30 | func (srmt *SlaveRequestMatrixType) Valid() bool {
31 | if srmt == nil {
32 | return false
33 | }
34 |
35 | switch *srmt {
36 | case MatrixAverageSpeed, MatrixMaxSpeed, MatrixPerSecondSpeed,
37 | MatrixUDPType,
38 | MatrixInboundGeoIP, MatrixOutboundGeoIP,
39 | MatrixScriptTest, MatrixHTTPPing, MatrixRTTPing,
40 | MatrixMAXHTTPPing, MatrixMAXRTTPing,
41 | MatrixTotalHTTPPing, MatrixTotalRTTPing,
42 | MatrixSDRTT, MatrixSDHTTP, MatrixHTTPCode, MatrixPacketLoss:
43 | return true
44 | }
45 |
46 | return false
47 | }
48 |
49 | // Matrix is the the atom attribute for a job
50 | // e.g. to fetch the RTTPing of a node,
51 | // it calls RTTPing matrix, which would initiate
52 | // a ping macro and return the RTTPing attribute
53 | type SlaveRequestMatrix interface {
54 | // define the matrix type to match
55 | Type() SlaveRequestMatrixType
56 |
57 | // define which macro job to run
58 | MacroJob() SlaveRequestMacroType
59 |
60 | // define the function to extract attribute
61 | // from macro result
62 | Extract(SlaveRequestMatrixEntry, SlaveRequestMacro)
63 | }
64 |
65 | type MatrixResponse struct {
66 | Type SlaveRequestMatrixType
67 | Payload string
68 | }
69 |
--------------------------------------------------------------------------------
/interfaces/matrix_fields.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type InvalidDS struct{}
4 |
5 | type HTTPPingDS struct {
6 | Value uint16
7 | }
8 |
9 | type RTTPingDS struct {
10 | Value uint16
11 | }
12 |
13 | type AverageSpeedDS struct {
14 | Value uint64
15 | }
16 |
17 | type MaxSpeedDS struct {
18 | Value uint64
19 | }
20 |
21 | type MaxRTTDS struct {
22 | Value uint16
23 | }
24 |
25 | type HTTPStatusCodeDS struct {
26 | Values []int
27 | }
28 | type MaxHTTPDS struct {
29 | Value uint16
30 | }
31 |
32 | type SDRTTDS struct {
33 | Value float64
34 | }
35 | type SDHTTPDS struct {
36 | Value float64
37 | }
38 | type PacketLossDS struct {
39 | Value float64
40 | }
41 |
42 | type PerSecondSpeedDS struct {
43 | Max uint64
44 | Average uint64
45 | Speeds []uint64
46 | }
47 |
48 | type TotalRTTDS struct {
49 | Values []uint16
50 | }
51 |
52 | type TotalHTTPDS struct {
53 | Values []uint16
54 | }
55 |
56 | type UDPTypeDS struct {
57 | Value string
58 | }
59 |
60 | type ScriptTestDS struct {
61 | Key string
62 | ScriptResult
63 | }
64 |
65 | type InboundGeoIPDS struct {
66 | MultiStacks
67 | }
68 |
69 | type OutboundGeoIPDS struct {
70 | MultiStacks
71 | }
72 |
--------------------------------------------------------------------------------
/interfaces/misc.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type RequestOptionsNetwork string
4 |
5 | const (
6 | ROptionsTCP RequestOptionsNetwork = "tcp"
7 | ROptionsTCP6 RequestOptionsNetwork = "tcp6"
8 | )
9 |
10 | func (ron *RequestOptionsNetwork) String() string {
11 | if ron == nil {
12 | return "tcp"
13 | }
14 |
15 | switch *ron {
16 | case ROptionsTCP:
17 | return "tcp"
18 | case ROptionsTCP6:
19 | return "tcp6"
20 | }
21 |
22 | return "tcp"
23 | }
24 |
25 | type RequestOptions struct {
26 | Method string
27 | URL string
28 | Headers map[string]string
29 | Cookies map[string]string
30 | Body []byte
31 | NoRedir bool
32 | Network RequestOptionsNetwork
33 | }
34 |
--------------------------------------------------------------------------------
/interfaces/proxy.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | import "github.com/airportr/miaospeed/utils/structs"
4 |
5 | type ProxyType string
6 |
7 | const (
8 | Shadowsocks ProxyType = "Shadowsocks"
9 | ShadowsocksR ProxyType = "ShadowsocksR"
10 | Snell ProxyType = "Snell"
11 | Socks5 ProxyType = "Socks5"
12 | Http ProxyType = "Http"
13 | Vmess ProxyType = "Vmess"
14 | Trojan ProxyType = "Trojan"
15 |
16 | Vless ProxyType = "Vless"
17 | Hysteria ProxyType = "Hysteria"
18 | Hysteria2 ProxyType = "Hysteria2"
19 | TUIC ProxyType = "TUIC"
20 | Wireguard ProxyType = "Wireguard"
21 | SSH ProxyType = "SSH"
22 | Mieru ProxyType = "Mieru"
23 | AnyTLS ProxyType = "AnyTLS"
24 |
25 | ProxyInvalid ProxyType = "Invalid"
26 | )
27 |
28 | var AllProxyTypes = []ProxyType{
29 | Shadowsocks, ShadowsocksR, Snell, Socks5, Http, Vmess, Trojan,
30 | Vless, Hysteria, Hysteria2, TUIC, Wireguard, SSH, Mieru, AnyTLS,
31 | }
32 |
33 | func Valid(proxyType ProxyType) bool {
34 | return structs.Contains(AllProxyTypes, proxyType)
35 | }
36 |
37 | func Parse(proxyType string) ProxyType {
38 | pType := ProxyType(proxyType)
39 | if Valid(pType) {
40 | return pType
41 | }
42 | return ProxyInvalid
43 | }
44 |
45 | type ProxyInfo struct {
46 | Name string
47 | Address string
48 | Type ProxyType
49 | }
50 |
51 | func (pi *ProxyInfo) Map() map[string]string {
52 | return map[string]string{
53 | "Name": pi.Name,
54 | "Address": pi.Address,
55 | "Type": string(pi.Type),
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/interfaces/scripts.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type ScriptType string
4 |
5 | const (
6 | STypeMedia ScriptType = "media"
7 | STypeIP ScriptType = "ip"
8 | )
9 |
10 | type Script struct {
11 | ID string `yaml:"-" fw:"readonly"`
12 | Type ScriptType `yaml:"type"`
13 | Content string `yaml:"content"`
14 | TimeoutMillis uint64 `yaml:"timeout,omitempty"`
15 | }
16 |
17 | type ScriptResult struct {
18 | Text string
19 | Color string
20 | Background string
21 | TimeElapsed int64
22 | }
23 |
24 | func (sr *ScriptResult) Clone() *ScriptResult {
25 | return &ScriptResult{
26 | Text: sr.Text,
27 | Color: sr.Color,
28 | Background: sr.Background,
29 | TimeElapsed: sr.TimeElapsed,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/interfaces/utils.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | func cloneSlice[T any](slice []T) []T {
4 | if slice == nil {
5 | return nil
6 | }
7 |
8 | return slice[:]
9 | }
10 |
--------------------------------------------------------------------------------
/interfaces/vendor.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | import (
4 | "context"
5 | "github.com/metacubex/mihomo/constant"
6 | "net"
7 | )
8 |
9 | type VendorType string
10 |
11 | const (
12 | VendorLocal VendorType = "Local"
13 | VendorClash VendorType = "Clash"
14 |
15 | VendorInvalid VendorType = "Invalid"
16 | )
17 |
18 | type VendorStatus uint
19 |
20 | const (
21 | VStatusOperational VendorStatus = iota
22 |
23 | VStatusNotReady
24 | )
25 |
26 | // a Vendor is an interface that allow macros to
27 | // trigger connections from
28 | type Vendor interface {
29 | // returns the type of the vendor
30 | Type() VendorType
31 |
32 | // returns the status of the vendor
33 | Status() VendorStatus
34 |
35 | // build conn based on proxy info string
36 | Build(proxyName string, proxyInfo string) Vendor
37 |
38 | // tcp connections
39 | DialTCP(ctx context.Context, url string, network RequestOptionsNetwork) (net.Conn, error)
40 |
41 | // udp connections
42 | DialUDP(ctx context.Context, url string) (net.PacketConn, error)
43 |
44 | // return universal proxy info
45 | ProxyInfo() ProxyInfo
46 |
47 | // raw proxt constant
48 | Proxy() constant.Proxy
49 | }
50 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/airportr/miaospeed/utils"
5 | )
6 |
7 | var COMPILATIONTIME string
8 | var BUILDCOUNT string
9 | var COMMIT string
10 | var BRAND string
11 | var VERSION string
12 |
13 | func main() {
14 | utils.COMPILATIONTIME = COMPILATIONTIME
15 | utils.BUILDCOUNT = BUILDCOUNT
16 | utils.COMMIT = COMMIT
17 | utils.BRAND = BRAND
18 | utils.VERSION = VERSION
19 | RunCli()
20 | }
21 |
--------------------------------------------------------------------------------
/misc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/preconfigs/certs.go:
--------------------------------------------------------------------------------
1 | package preconfigs
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | )
7 |
8 | func MakeSelfSignedTLSServer() *tls.Config {
9 | cert, _ := tls.X509KeyPair([]byte(MIAOKO_TLS_CRT), []byte(MIAOKO_TLS_KEY))
10 |
11 | // Construct a tls.config
12 | tlsConfig := &tls.Config{
13 | Certificates: []tls.Certificate{cert},
14 | // Other options
15 | }
16 |
17 | return tlsConfig
18 | }
19 |
20 | func MiaokoRootCAPrepare() *x509.CertPool {
21 | rootCAs := x509.NewCertPool()
22 | rootCAs.AppendCertsFromPEM(MIAOKO_ROOT_CA)
23 | return rootCAs
24 | }
25 |
--------------------------------------------------------------------------------
/preconfigs/embeded.go:
--------------------------------------------------------------------------------
1 | package preconfigs
2 |
3 | import (
4 | _ "embed"
5 | )
6 |
7 | //go:embed embeded/miaokoCA/miaoko.crt
8 | var MIAOKO_TLS_CRT string
9 |
10 | //go:embed embeded/miaokoCA/miaoko.key
11 | var MIAOKO_TLS_KEY string
12 |
13 | //go:embed embeded/ca-certificates.crt
14 | var MIAOKO_ROOT_CA []byte
15 |
--------------------------------------------------------------------------------
/preconfigs/embeded/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AirportR/miaospeed/8850c39b0ff504bb752d6ffcbfaa1c3bae3c35bc/preconfigs/embeded/.gitkeep
--------------------------------------------------------------------------------
/preconfigs/network.go:
--------------------------------------------------------------------------------
1 | package preconfigs
2 |
3 | const PROXY_DEFAULT_STUN_SERVER = "udp://stun.voipstunt.com:3478"
4 |
5 | const NETCAT_HTTP_PAYLOAD = `GET %s HTTP/1.1
6 | Accept: */*
7 | Accept-Encoding: gzip, deflate
8 | Host: %s
9 | User-Agent: HTTPie/3.0.2 MiaoSpeed/%s
10 |
11 | `
12 |
13 | const (
14 | SPEED_DEFAULT_DURATION int64 = 3
15 | SPEED_DEFAULT_THREADING uint = 1
16 |
17 | SPEED_DEFAULT_LARGE_FILE_STATIC_APPLE string = "https://updates.cdn-apple.com/2019FallFCS/fullrestores/061-22552/374D62DE-E18B-11E9-A68D-B46496A9EC6E/iPhone12,1_13.1.2_17A860_Restore.ipsw"
18 | SPEED_DEFAULT_LARGE_FILE_STATIC_MSFT string = "https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe"
19 | SPEED_DEFAULT_LARGE_FILE_STATIC_GOOGLE string = "https://dl.google.com/android/studio/maven-google-com/stable/offline-gmaven-stable.zip"
20 | SPEED_DEFAULT_LARGE_FILE_STATIC_CACHEFLY string = "http://cachefly.cachefly.net/200mb.test"
21 |
22 | SPEED_DEFAULT_LARGE_FILE_DYN_INTL string = "DYNAMIC:INTL"
23 | SPEED_DEFAULT_LARGE_FILE_DYN_FAST string = "DYNAMIC:FAST"
24 |
25 | SPEED_DEFAULT_LARGE_FILE_DEFAULT = SPEED_DEFAULT_LARGE_FILE_DYN_INTL
26 |
27 | SLAVE_DEFAULT_PING = "http://gstatic.com/generate_204"
28 | SLAVE_DEFAULT_RETRY uint = 3
29 | SLAVE_DEFAULT_TIMEOUT uint = 5000
30 | )
31 |
--------------------------------------------------------------------------------
/service/macros/geo/engine.go:
--------------------------------------------------------------------------------
1 | package geo
2 |
3 | import (
4 | "net"
5 | "strings"
6 |
7 | "github.com/airportr/miaospeed/engine"
8 | "github.com/airportr/miaospeed/engine/helpers"
9 | "github.com/airportr/miaospeed/interfaces"
10 | )
11 |
12 | func ExecIpCheck(p interfaces.Vendor, script string, network interfaces.RequestOptionsNetwork) (ipstacks *interfaces.IPStacks) {
13 | ipstacks = (&interfaces.IPStacks{}).Init()
14 |
15 | vm := engine.VMNewWithVendor(p, network)
16 | vm.RunString(engine.PREDEFINED_SCRIPT + engine.DEFAULT_IP_SCRIPT + script)
17 | caller := "ip_resolve_default"
18 | if engine.HasFunction(vm, "ip_resolve") {
19 | caller = "ip_resolve"
20 | }
21 |
22 | ret, err := engine.ExecTaskCallback(vm, caller)
23 | if engine.ThrowExecTaskErr("IPResolve", err) {
24 | return
25 | } else {
26 | ipQuery := []string{}
27 | helpers.VMSafeMarshal(&ipQuery, ret, vm)
28 | for _, ip := range ipQuery {
29 | if net.ParseIP(ip) != nil {
30 | if !strings.Contains(ip, ":") {
31 | ipstacks.IPv4 = append(ipstacks.IPv4, ip)
32 | } else {
33 | ipstacks.IPv6 = append(ipstacks.IPv6, ip)
34 | }
35 | }
36 | }
37 | }
38 |
39 | return
40 | }
41 |
42 | func ExecGeoCheck(p interfaces.Vendor, script string, ip string, network interfaces.RequestOptionsNetwork) *interfaces.GeoInfo {
43 | vm := engine.VMNewWithVendor(p, network)
44 | if script == "" {
45 | script = engine.DEFAULT_GEOIP_SCRIPT
46 | }
47 | vm.RunString(engine.PREDEFINED_SCRIPT + script)
48 |
49 | ret, err := engine.ExecTaskCallback(vm, "handler", ip)
50 | if engine.ThrowExecTaskErr("GeoCheck", err) {
51 | return nil
52 | } else {
53 | geoInfo := &interfaces.GeoInfo{}
54 | if err := helpers.VMSafeMarshal(geoInfo, ret, vm); err == nil {
55 | return geoInfo
56 | }
57 | }
58 |
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/service/macros/geo/geo.go:
--------------------------------------------------------------------------------
1 | package geo
2 |
3 | import (
4 | "net"
5 | "strings"
6 |
7 | "github.com/airportr/miaospeed/interfaces"
8 | "github.com/airportr/miaospeed/utils"
9 | )
10 |
11 | func RemoteLookup(p interfaces.Vendor, script string, retry int) *interfaces.IPStacks {
12 | ret := &interfaces.IPStacks{}
13 | for i := 0; i < retry && ret.Count() == 0; i++ {
14 | ret = ExecIpCheck(p, script, interfaces.ROptionsTCP)
15 | }
16 | return ret
17 | }
18 |
19 | type DetectSourceMode int
20 |
21 | const (
22 | DSMDefault DetectSourceMode = iota
23 | DSMInOnly
24 | DSMOutOnly
25 | )
26 |
27 | func DetectingSource(p interfaces.Vendor, script string, retry int, queryServers []string, mode DetectSourceMode) (in *interfaces.MultiStacks, out *interfaces.MultiStacks) {
28 | if mode == DSMOutOnly || mode == DSMDefault {
29 | out = &interfaces.MultiStacks{
30 | Domain: p.ProxyInfo().Name,
31 | IPv4Stack: make([]*interfaces.GeoInfo, 0),
32 | IPv6Stack: make([]*interfaces.GeoInfo, 0),
33 | MainStack: &interfaces.GeoInfo{},
34 | }
35 |
36 | outIpstacks := RemoteLookup(p, script, retry)
37 | for _, outv6Ip := range outIpstacks.IPv6 {
38 | if outv6 := RunGeoCheck(p, script, outv6Ip, retry, interfaces.ROptionsTCP6); outv6 != nil {
39 | out.IPv6Stack = append(out.IPv6Stack, outv6)
40 | out.MainStack = outv6
41 | }
42 | }
43 |
44 | for _, outv4Ip := range outIpstacks.IPv4 {
45 | if outv4 := RunGeoCheck(p, script, outv4Ip, retry, interfaces.ROptionsTCP); outv4 != nil {
46 | out.IPv4Stack = append(out.IPv4Stack, outv4)
47 | out.MainStack = outv4
48 | }
49 | }
50 | }
51 |
52 | if mode == DSMInOnly || mode == DSMDefault {
53 | inIP := p.ProxyInfo().Address
54 | if strings.Count(inIP, ":") > 1 {
55 | // ipv6
56 | inIP = p.ProxyInfo().Address
57 | } else {
58 | // domain
59 | inIP = strings.Split(p.ProxyInfo().Address, ":")[0]
60 | }
61 |
62 | domain := inIP
63 | ipv4 := []string{}
64 | ipv6 := []string{}
65 | if net.ParseIP(inIP) == nil {
66 | ipstacks := utils.LookupIPv46(inIP, retry, queryServers)
67 | ipv4 = ipstacks.IPv4
68 | ipv6 = ipstacks.IPv6
69 | } else {
70 | if strings.Contains(inIP, ":") {
71 | ipv6 = []string{inIP}
72 | } else {
73 | ipv4 = []string{inIP}
74 | }
75 | }
76 |
77 | in = &interfaces.MultiStacks{
78 | Domain: domain,
79 | IPv4Stack: make([]*interfaces.GeoInfo, 0),
80 | IPv6Stack: make([]*interfaces.GeoInfo, 0),
81 | MainStack: &interfaces.GeoInfo{},
82 | }
83 | var pcopy interfaces.Vendor = nil
84 | // if count > 0 , it means the current proxy is valid.
85 | if out != nil && out.Count() > 0 {
86 | pcopy = p
87 | }
88 |
89 | for _, ip := range ipv4 {
90 | in.IPv4Stack = append(in.IPv4Stack, RunGeoCheck(pcopy, script, ip, retry, "tcp"))
91 | }
92 | for _, ip := range ipv6 {
93 | in.IPv6Stack = append(in.IPv6Stack, RunGeoCheck(pcopy, script, ip, retry, "tcp"))
94 | }
95 |
96 | if in.Count() > 0 {
97 | if len(in.IPv4Stack) > 0 {
98 | in.MainStack = in.IPv4Stack[0]
99 | } else {
100 | in.MainStack = in.IPv6Stack[0]
101 | }
102 | }
103 | }
104 |
105 | return
106 | }
107 |
--------------------------------------------------------------------------------
/service/macros/geo/geocheck.go:
--------------------------------------------------------------------------------
1 | package geo
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/airportr/miaospeed/interfaces"
7 | "github.com/airportr/miaospeed/utils"
8 | "github.com/airportr/miaospeed/utils/structs"
9 | "github.com/airportr/miaospeed/utils/structs/memutils"
10 | "github.com/airportr/miaospeed/utils/structs/obliviousmap"
11 | )
12 |
13 | var GeoCache *obliviousmap.ObliviousMap[*interfaces.GeoInfo]
14 |
15 | func RunGeoCheck(p interfaces.Vendor, script string, ip string, retry int, network interfaces.RequestOptionsNetwork) *interfaces.GeoInfo {
16 | var ret *interfaces.GeoInfo = nil
17 | if r, ok := GeoCache.Get(ip); ok && r != nil {
18 | return r
19 | }
20 |
21 | // use mmdb first, if cannot get record, try remote query 3 times
22 | if ret = RunMMDBCheck(ip); ret == nil {
23 | utils.DLog("use the remote GEOAPI...")
24 | for i := 0; i < structs.WithIn(retry, 1, 3) && (ret == nil || ret.IP == ""); i++ {
25 | ret = ExecGeoCheck(p, script, ip, network)
26 | }
27 | }
28 |
29 | if ret == nil {
30 | ret = &interfaces.GeoInfo{}
31 | }
32 |
33 | proxyName := "NoProxy"
34 | if p != nil {
35 | proxyName = p.ProxyInfo().Name
36 | }
37 |
38 | if ret != nil && ret.IP != "" {
39 | GeoCache.Set(ret.IP, ret)
40 | utils.DLogf("GetIP Resolver | Resolved IP=%s proxy=%v ASN=%d ASOrg=%s", ip, proxyName, ret.ASN, ret.ASNOrg)
41 | } else {
42 | utils.DWarnf("GeoIP Resolver | Fail to resolve IP=%s proxy=%v", ip, proxyName)
43 | }
44 | return ret
45 | }
46 |
47 | func init() {
48 | memGeoInfo := memutils.MemDriverMemory[*interfaces.GeoInfo]{}
49 | memGeoInfo.Init()
50 | GeoCache = obliviousmap.NewObliviousMap[*interfaces.GeoInfo]("GeoCache/", time.Hour*6, true, &memGeoInfo)
51 | }
52 |
--------------------------------------------------------------------------------
/service/macros/geo/macro.go:
--------------------------------------------------------------------------------
1 | package geo
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/utils/structs"
6 | )
7 |
8 | type Geo struct {
9 | InStacks interfaces.MultiStacks
10 | OutStacks interfaces.MultiStacks
11 | }
12 |
13 | func (m *Geo) Type() interfaces.SlaveRequestMacroType {
14 | return interfaces.MacroGeo
15 | }
16 |
17 | func (m *Geo) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
18 | ipScripts := structs.Filter(r.Configs.Scripts, func(v interfaces.Script) bool {
19 | return v.Type == interfaces.STypeIP
20 | })
21 |
22 | ipScript := ""
23 | if len(ipScripts) > 0 {
24 | ipScript = ipScripts[0].Content
25 | }
26 |
27 | inStacks, outStacks := DetectingSource(proxy, ipScript, 3, r.Configs.DNSServers, DSMDefault)
28 | if inStacks != nil {
29 | m.InStacks = *inStacks
30 | }
31 | if outStacks != nil {
32 | m.OutStacks = *outStacks
33 | }
34 |
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/service/macros/geo/mmdb.go:
--------------------------------------------------------------------------------
1 | package geo
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/utils"
6 | )
7 |
8 | func RunMMDBCheck(rawIp string) *interfaces.GeoInfo {
9 | if record := utils.QueryMaxMindDB(rawIp); record != nil {
10 | return &interfaces.GeoInfo{
11 | ASN: record.ASN,
12 | ASNOrg: record.ASNOrg,
13 | Org: record.ASNOrg,
14 | ISP: record.ASNOrg, // inaccurate, just fallback
15 | IP: rawIp,
16 |
17 | Country: record.Country.Names.EN,
18 | CountryCode: record.Country.ISOCode,
19 | ContinentCode: record.Continent.Code,
20 | TimeZone: record.Location.TimeZone,
21 | Lat: record.Location.Latitude,
22 | Lon: record.Location.Longitude,
23 | }
24 | }
25 |
26 | return nil
27 | }
28 |
--------------------------------------------------------------------------------
/service/macros/invalid/macro.go:
--------------------------------------------------------------------------------
1 | package invalid
2 |
3 | import "github.com/airportr/miaospeed/interfaces"
4 |
5 | type Invalid struct{}
6 |
7 | func (m *Invalid) Type() interfaces.SlaveRequestMacroType {
8 | return interfaces.MacroInvalid
9 | }
10 |
11 | func (m *Invalid) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
12 | return nil
13 | }
14 |
--------------------------------------------------------------------------------
/service/macros/macros.go:
--------------------------------------------------------------------------------
1 | package macros
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/utils/structs"
6 |
7 | "github.com/airportr/miaospeed/service/macros/geo"
8 | "github.com/airportr/miaospeed/service/macros/invalid"
9 | "github.com/airportr/miaospeed/service/macros/ping"
10 | "github.com/airportr/miaospeed/service/macros/script"
11 | "github.com/airportr/miaospeed/service/macros/sleep"
12 | "github.com/airportr/miaospeed/service/macros/speed"
13 | "github.com/airportr/miaospeed/service/macros/udp"
14 | )
15 |
16 | var registeredList = map[interfaces.SlaveRequestMacroType]func() interfaces.SlaveRequestMacro{
17 | interfaces.MacroSpeed: func() interfaces.SlaveRequestMacro {
18 | return &speed.Speed{}
19 | },
20 | interfaces.MacroPing: func() interfaces.SlaveRequestMacro {
21 | return &ping.Ping{}
22 | },
23 | interfaces.MacroUDP: func() interfaces.SlaveRequestMacro {
24 | return &udp.Udp{}
25 | },
26 | interfaces.MacroGeo: func() interfaces.SlaveRequestMacro {
27 | return &geo.Geo{}
28 | },
29 | interfaces.MacroScript: func() interfaces.SlaveRequestMacro { return &script.Script{} },
30 | interfaces.MacroSleep: func() interfaces.SlaveRequestMacro { return &sleep.Sleep{} },
31 | }
32 |
33 | func Find(macroType interfaces.SlaveRequestMacroType) interfaces.SlaveRequestMacro {
34 | if newFn, ok := registeredList[macroType]; ok && newFn != nil {
35 | return newFn()
36 | }
37 |
38 | return &invalid.Invalid{}
39 | }
40 |
41 | func FindBatch(macroTypes []interfaces.SlaveRequestMacroType) []interfaces.SlaveRequestMacro {
42 | return structs.Map(macroTypes, func(m interfaces.SlaveRequestMacroType) interfaces.SlaveRequestMacro {
43 | return Find(m)
44 | })
45 | }
46 |
--------------------------------------------------------------------------------
/service/macros/ping/macro.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | )
6 |
7 | type Ping struct {
8 | RTT uint16
9 | Request uint16
10 | MaxRTT uint16
11 | MaxRequest uint16
12 | RTTSD float64
13 | RequestSD float64
14 | RTTList []uint16
15 | RequestList []uint16
16 | StatusCodes []int
17 | PacketLoss float64
18 | Jitter float64
19 | }
20 |
21 | func (m *Ping) Type() interfaces.SlaveRequestMacroType {
22 | return interfaces.MacroPing
23 | }
24 |
25 | func (m *Ping) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
26 | ping(m, proxy, r.Configs.PingAddress, r.Configs.PingAverageOver, r.Configs.TaskTimeout)
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/service/macros/ping/ping.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "crypto/tls"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "net"
11 | "net/http"
12 | "net/http/httptrace"
13 | urllib "net/url"
14 | "strings"
15 | "time"
16 |
17 | "github.com/airportr/miaospeed/interfaces"
18 | "github.com/airportr/miaospeed/preconfigs"
19 | "github.com/airportr/miaospeed/utils"
20 | "github.com/airportr/miaospeed/utils/structs"
21 | )
22 |
23 | type timeoutReader struct {
24 | r *bufio.Reader
25 | timeout time.Time
26 | }
27 |
28 | func (tr *timeoutReader) Read(p []byte) (n int, err error) {
29 | if time.Now().After(tr.timeout) {
30 | return 0, errors.New("read timeout")
31 | }
32 | return tr.r.Read(p)
33 | }
34 |
35 | func saferParseHTTPStatus(reader *bufio.Reader) (int, error) {
36 | timeoutReader := &timeoutReader{reader, time.Now().Add(5 * time.Second)}
37 | limitedReader := io.LimitReader(timeoutReader, 1024*1024)
38 |
39 | resp, err := http.ReadResponse(bufio.NewReader(limitedReader), nil)
40 | if err != nil {
41 | return 0, err
42 | }
43 | //defer resp.Body.Close()
44 |
45 | return resp.StatusCode, nil
46 | }
47 |
48 | func pingViaTrace(ctx context.Context, p interfaces.Vendor, url string) (uint16, uint16, int, error) {
49 | transport := &http.Transport{
50 | DialContext: func(context.Context, string, string) (net.Conn, error) {
51 | return p.DialTCP(ctx, url, interfaces.ROptionsTCP)
52 | },
53 | MaxIdleConns: 100,
54 | IdleConnTimeout: 3 * time.Second,
55 | TLSHandshakeTimeout: 3 * time.Second,
56 | ExpectContinueTimeout: 1 * time.Second,
57 | TLSClientConfig: &tls.Config{
58 | InsecureSkipVerify: false,
59 | // for version prior to tls1.3, the handshake will take 2-RTTs,
60 | // plus, majority server supports tls1.3, so we set a limit here.
61 | MinVersion: tls.VersionTLS13,
62 | RootCAs: preconfigs.MiaokoRootCAPrepare(),
63 | },
64 | }
65 |
66 | req, err := http.NewRequest("GET", url, nil)
67 | if err != nil {
68 | return 0, 0, 0, err
69 | }
70 |
71 | var tlsStart, tlsEnd, writeStart, writeEnd int64
72 | trace := &httptrace.ClientTrace{
73 | TLSHandshakeStart: func() { tlsStart = time.Now().UnixMilli() },
74 | TLSHandshakeDone: func(_ tls.ConnectionState, err error) { tlsEnd = time.Now().UnixMilli() },
75 | GotFirstResponseByte: func() { writeEnd = time.Now().UnixMilli() },
76 | WroteHeaders: func() { writeStart = time.Now().UnixMilli() },
77 | }
78 | req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
79 |
80 | connStart := time.Now().UnixMilli()
81 | resp, err := transport.RoundTrip(req)
82 | if err != nil {
83 | return 0, 0, 0, err
84 | }
85 | defer resp.Body.Close()
86 |
87 | connEnd := time.Now().UnixMilli()
88 | utils.DBlackhole(!strings.HasPrefix(url, "https:"), connEnd-writeEnd, writeEnd-tlsEnd, tlsEnd-tlsStart, tlsStart-connStart)
89 |
90 | if !strings.HasPrefix(url, "https:") {
91 | return uint16(writeStart - connStart), uint16(writeEnd - connStart), resp.StatusCode, nil
92 | }
93 | if resp.TLS != nil && resp.TLS.HandshakeComplete {
94 | return uint16(writeEnd - tlsEnd), uint16(writeEnd - connStart), resp.StatusCode, nil
95 | }
96 | return 0, 0, 0, fmt.Errorf("cannot extract payload from response")
97 | }
98 |
99 | func pingViaNetCat(ctx context.Context, p interfaces.Vendor, url string) (uint16, uint16, int, error) {
100 | purl, _ := urllib.Parse(url)
101 | path := purl.EscapedPath()
102 | if purl.RawQuery != "" {
103 | path += "?" + purl.Query().Encode()
104 | }
105 | payload := structs.X(preconfigs.NETCAT_HTTP_PAYLOAD, path, purl.Hostname(), utils.VERSION)
106 |
107 | connStart := time.Now()
108 | conn, err := p.DialTCP(ctx, url, interfaces.ROptionsTCP)
109 | if err != nil {
110 | return 0, 0, 0, fmt.Errorf("dial failed: %w", err)
111 | }
112 | defer conn.Close()
113 |
114 | _ = conn.SetDeadline(time.Now().Add(6 * time.Second))
115 | reader := bufio.NewReader(conn)
116 | tcpStart := time.Now()
117 | if _, err := conn.Write([]byte(payload)); err != nil {
118 | return 0, 0, 0, fmt.Errorf("write failed 1: %w", err)
119 | }
120 |
121 | _, _ = reader.Peek(1) // Flush buffer
122 | tcpRTT := time.Since(tcpStart).Milliseconds()
123 | connRTT := time.Since(connStart).Milliseconds()
124 | statusCode, err := saferParseHTTPStatus(reader)
125 | //_, _, _ = reader.ReadLine()
126 | for reader.Buffered() > 0 {
127 | _, _, _ = reader.ReadLine()
128 | }
129 | tcpStart = time.Now()
130 | if _, err := conn.Write([]byte(payload)); err != nil {
131 | return 0, 0, 0, fmt.Errorf("write failed 2: %w", err)
132 | }
133 | if _, err := reader.Peek(1); err != nil {
134 | if err == io.EOF {
135 | return uint16(tcpRTT), uint16(connRTT), statusCode, nil
136 | }
137 |
138 | return uint16(tcpRTT), uint16(connRTT), statusCode, fmt.Errorf("read failed 2: %w", err)
139 | }
140 | // resend the request to get the RTT of the second request
141 | tcpRTT = time.Since(tcpStart).Milliseconds()
142 | statusCode, err = saferParseHTTPStatus(reader)
143 | if err != nil {
144 | return uint16(tcpRTT), 0, 0, nil
145 | }
146 | return uint16(tcpRTT), uint16(connRTT), statusCode, nil
147 | }
148 |
149 | func ping(obj *Ping, p interfaces.Vendor, url string, withAvg uint16, timeout uint) {
150 | var (
151 | rttTimes []uint16
152 | requestTimes []uint16
153 | statusCodes []int
154 | failedAttempt uint
155 | )
156 |
157 | if p == nil {
158 | initFailedPing(obj)
159 | return
160 | }
161 |
162 | for i := 0; i < int(withAvg); i++ {
163 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Millisecond)
164 | rtt, req, code, err := performPing(ctx, p, url)
165 | cancel()
166 |
167 | if err != nil {
168 | //utils.DLogf("ping failed: %v", err)
169 | failedAttempt++
170 | continue
171 | }
172 |
173 | updatePingMetrics(obj, rtt, req)
174 | rttTimes = append(rttTimes, rtt)
175 | requestTimes = append(requestTimes, req)
176 | statusCodes = append(statusCodes, code)
177 | }
178 |
179 | calculateFinalMetrics(obj, rttTimes, requestTimes, statusCodes, failedAttempt, withAvg)
180 | }
181 |
182 | func initFailedPing(obj *Ping) {
183 | obj.RTT = 0
184 | obj.Request = 0
185 | obj.PacketLoss = 100.0
186 | obj.Jitter = 0
187 | obj.RTTList = nil
188 | obj.RequestList = nil
189 | obj.StatusCodes = nil
190 | }
191 |
192 | func performPing(ctx context.Context, p interfaces.Vendor, url string) (uint16, uint16, int, error) {
193 | if strings.HasPrefix(url, "https:") {
194 | return pingViaTrace(ctx, p, url)
195 | }
196 | return pingViaNetCat(ctx, p, url)
197 | }
198 |
199 | func updatePingMetrics(obj *Ping, rtt, req uint16) {
200 | obj.MaxRTT = structs.Max(obj.MaxRTT, rtt)
201 | obj.MaxRequest = structs.Max(obj.MaxRequest, req)
202 | }
203 |
204 | func calculateFinalMetrics(obj *Ping, rtts, reqs []uint16, codes []int, failed uint, total uint16) {
205 | obj.PacketLoss = float64(failed) / float64(total) * 100
206 | obj.StatusCodes = codes
207 |
208 | if len(rtts) > 0 {
209 | obj.RTT = calcAvgPing(rtts)
210 | obj.Jitter = calcStdDevPing(rtts)
211 | obj.RTTSD = obj.Jitter
212 | obj.RTTList = rtts
213 | }
214 | if len(reqs) > 0 {
215 | obj.Request = calcAvgPing(reqs)
216 | obj.RequestSD = calcStdDevPing(reqs)
217 | obj.RequestList = reqs
218 | }
219 | }
220 |
221 | //func calcAvgPing(values []uint16) uint16 {
222 | // if len(values) == 0 {
223 | // return 0
224 | // }
225 | // var sum uint32
226 | // for _, v := range values {
227 | // sum += uint32(v)
228 | // }
229 | // return uint16(sum / uint32(len(values)))
230 | //}
231 | //
232 | //func calcStdDevPing(values []uint16) uint16 {
233 | // if len(values) < 2 {
234 | // return 0
235 | // }
236 | // mean := calcAvgPing(values)
237 | // var variance float64
238 | // for _, v := range values {
239 | // diff := float64(int32(v) - int32(mean))
240 | // variance += diff * diff
241 | // }
242 | // variance /= float64(len(values) - 1)
243 | // return uint16(variance)
244 | //}
245 |
--------------------------------------------------------------------------------
/service/macros/ping/utils.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "math"
5 | "sort"
6 | )
7 | import "github.com/airportr/miaospeed/utils"
8 |
9 | func computeAvgOfPing(pings []uint16) uint16 {
10 | result := uint16(0)
11 | totalMS := pings[:]
12 | sort.Slice(totalMS, func(i, j int) bool { return totalMS[i] < totalMS[j] })
13 | mediumMS := totalMS[len(totalMS)/2]
14 | threshold := 300
15 | realCount := uint16(0)
16 | for _, delay := range totalMS {
17 | if -threshold < int(delay)-int(mediumMS) && int(delay)-int(mediumMS) < threshold {
18 | realCount += 1
19 | }
20 | }
21 | if realCount == 0 {
22 | return 0
23 | }
24 | for _, delay := range totalMS {
25 | if -threshold < int(delay)-int(mediumMS) && int(delay)-int(mediumMS) < threshold {
26 | result += delay / realCount
27 | }
28 | }
29 | return result
30 | }
31 |
32 | func calcAvgPing(values []uint16) uint16 {
33 | result := uint16(0)
34 | totalMS := values[:]
35 | var nonZeroLatencies []uint16
36 | if len(values) == 0 {
37 | return 0
38 | }
39 | // 移除0值
40 | for _, lat := range totalMS {
41 | if lat != 0 {
42 | nonZeroLatencies = append(nonZeroLatencies, lat)
43 | }
44 | }
45 |
46 | // 如果切片为空,返回0
47 | if len(nonZeroLatencies) == 0 {
48 | return 0
49 | }
50 |
51 | // 如果切片只有一个元素,直接返回该元素
52 | if len(nonZeroLatencies) == 1 {
53 | return nonZeroLatencies[0]
54 | }
55 | if len(nonZeroLatencies) == 2 {
56 | return (nonZeroLatencies[0] + nonZeroLatencies[1]) / 2
57 | }
58 |
59 | // 排序切片
60 | sort.Slice(nonZeroLatencies, func(i, j int) bool { return totalMS[i] < totalMS[j] })
61 |
62 | // 移除最高和最低延迟
63 | trimmedLatencies := nonZeroLatencies[1 : len(nonZeroLatencies)-1]
64 |
65 | // 如果移除后切片为空,返回0
66 | if len(trimmedLatencies) == 0 {
67 | return 0
68 | }
69 |
70 | if len(trimmedLatencies) == 1 {
71 | return trimmedLatencies[0]
72 | }
73 |
74 | // 计算平均值
75 | sum := uint16(0)
76 | for _, lat := range trimmedLatencies {
77 | sum += lat
78 | }
79 | average := float64(sum) / float64(len(trimmedLatencies))
80 | result = uint16(average)
81 | return result
82 | }
83 |
84 | func calcStdDevPing(values []uint16) float64 {
85 | output := make([]float64, len(values))
86 | for i, v := range values {
87 | output[i] = float64(v)
88 | }
89 | return math.Round(utils.StandardDeviation(output)*10) / 10
90 | }
91 |
--------------------------------------------------------------------------------
/service/macros/script/engine.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "runtime"
5 | "time"
6 |
7 | "github.com/airportr/miaospeed/engine"
8 | "github.com/airportr/miaospeed/engine/helpers"
9 | "github.com/airportr/miaospeed/interfaces"
10 | "github.com/dop251/goja"
11 | )
12 |
13 | func ExecScript(p interfaces.Vendor, script *interfaces.Script) interfaces.ScriptResult {
14 | s := interfaces.ScriptResult{}
15 | if script == nil {
16 | return s
17 | }
18 |
19 | vm := engine.VMNewWithVendor(p, interfaces.ROptionsTCP)
20 |
21 | startTime := time.Now()
22 | ret, err := engine.RunWithTimeout(vm, time.Duration(script.TimeoutMillis)*time.Millisecond, func() (goja.Value, error) {
23 | vm.RunString(engine.PREDEFINED_SCRIPT + script.Content)
24 | return engine.ExecTaskCallback(vm, "handler")
25 | })
26 |
27 | s.TimeElapsed = time.Now().UnixMilli() - startTime.UnixMilli()
28 | if engine.ThrowExecTaskErr("MediaTest", err) {
29 | // nothing here
30 | } else if text, ok := helpers.VMSafeStr(ret); ok {
31 | s.Text = text
32 | } else if ro, _ := helpers.VMSafeObj(vm, ret); ro != nil {
33 | if v, ok := helpers.VMSafeStr(ro.Get("text")); ok {
34 | s.Text = v
35 | }
36 | if v, ok := helpers.VMSafeStr(ro.Get("color")); ok {
37 | s.Color = v
38 | }
39 | if v, ok := helpers.VMSafeStr(ro.Get("background")); ok {
40 | s.Background = v
41 | }
42 | }
43 |
44 | vm = nil
45 | runtime.GC()
46 |
47 | return s
48 | }
49 |
--------------------------------------------------------------------------------
/service/macros/script/macro.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "context"
5 | "os"
6 | "strconv"
7 | "sync"
8 |
9 | "github.com/airportr/miaospeed/interfaces"
10 | "github.com/airportr/miaospeed/utils/structs"
11 | "golang.org/x/sync/semaphore"
12 | )
13 |
14 | var scriptControl *semaphore.Weighted
15 |
16 | type Script struct {
17 | Store map[string]interfaces.ScriptResult
18 | }
19 |
20 | func (m *Script) Type() interfaces.SlaveRequestMacroType {
21 | return interfaces.MacroScript
22 | }
23 |
24 | func (m *Script) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
25 | store := structs.NewAsyncMap[string, interfaces.ScriptResult]()
26 | execScripts := structs.Filter(r.Configs.Scripts, func(v interfaces.Script) bool {
27 | return v.Type == interfaces.STypeMedia
28 | })
29 |
30 | wg := sync.WaitGroup{}
31 | wg.Add(len(execScripts))
32 | for i := range execScripts {
33 | script := &execScripts[i]
34 | go func() {
35 | scriptControl.Acquire(context.Background(), 1)
36 | defer scriptControl.Release(1)
37 |
38 | store.Set(script.ID, ExecScript(proxy, script))
39 | wg.Done()
40 | }()
41 | }
42 | wg.Wait()
43 |
44 | m.Store = store.ForEach()
45 | return nil
46 | }
47 |
48 | func init() {
49 | // default strict to 32 concurrent script engine
50 | // can be extended by setting env var
51 | concurrency, _ := strconv.ParseInt(os.Getenv("MIAOKO_SCRIPT_CONCURRENCY"), 10, 64)
52 | concurrency = structs.WithInDefault(concurrency, 1, 64, 32)
53 | scriptControl = semaphore.NewWeighted(concurrency)
54 | }
55 |
--------------------------------------------------------------------------------
/service/macros/sleep/macro.go:
--------------------------------------------------------------------------------
1 | package sleep
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "time"
6 | )
7 |
8 | type Sleep struct {
9 | duration time.Duration
10 | Value string
11 | }
12 |
13 | func (m *Sleep) Type() interfaces.SlaveRequestMacroType {
14 | return interfaces.MacroSleep
15 | }
16 |
17 | func (m *Sleep) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
18 | m.duration = time.Second * 10
19 | time.Sleep(m.duration)
20 | m.Value = "slept for " + m.duration.String() + " seconds"
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/service/macros/speed/macro.go:
--------------------------------------------------------------------------------
1 | package speed
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/utils"
6 | "time"
7 | )
8 |
9 | type Speed struct {
10 | AvgSpeed uint64
11 | MaxSpeed uint64
12 | TotalSize uint64
13 | Speeds []uint64
14 | }
15 |
16 | func (m *Speed) Type() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroSpeed
18 | }
19 |
20 | func (m *Speed) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
21 | t1 := time.Now()
22 | Once(m, proxy, &r.Configs)
23 | t2 := time.Now()
24 | utils.DLogf("Speed macro took %s", t2.Sub(t1))
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/service/macros/speed/speed.go:
--------------------------------------------------------------------------------
1 | package speed
2 |
3 | import (
4 | "context"
5 | "io"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | jsoniter "github.com/json-iterator/go"
11 | "github.com/juju/ratelimit"
12 |
13 | "github.com/airportr/miaospeed/interfaces"
14 | "github.com/airportr/miaospeed/preconfigs"
15 | "github.com/airportr/miaospeed/utils"
16 | "github.com/airportr/miaospeed/utils/structs"
17 | "github.com/airportr/miaospeed/vendors"
18 | )
19 |
20 | func Once(speed *Speed, proxy interfaces.Vendor, cfg *interfaces.SlaveRequestConfigsV2) {
21 | speed.Speeds = make([]uint64, cfg.DownloadDuration)
22 |
23 | downloadFiles := RefetchDownloadFiles(proxy, cfg.DownloadURL)
24 | utils.DLogf("Speed Prefetch | Using files arr=%v", downloadFiles)
25 |
26 | th := int(cfg.DownloadThreading)
27 | var wcGroups []*WriteCounter
28 | var ctxCancels []context.CancelFunc
29 |
30 | initWG := sync.WaitGroup{}
31 | writingLock := sync.Mutex{}
32 | for i := 0; i < th; i++ {
33 | initWG.Add(1)
34 | go func() {
35 | wc := WriteCounter{
36 | RateLimit: int64(utils.GCFG.SpeedLimit) / int64(th),
37 | }
38 | cancelFunc := SingleThread(downloadFiles, proxy, cfg.DownloadDuration, &wc)
39 |
40 | writingLock.Lock()
41 | ctxCancels = append(ctxCancels, cancelFunc)
42 | wcGroups = append(wcGroups, &wc)
43 | writingLock.Unlock()
44 |
45 | initWG.Done()
46 | }()
47 | }
48 | initWG.Wait()
49 |
50 | // normalization
51 | for i := 0; i < th; i++ {
52 | wcGroups[i].Take()
53 | }
54 |
55 | for t := 0; t < int(cfg.DownloadDuration); t++ {
56 | time.Sleep(time.Second - time.Millisecond*10)
57 | byteLen := uint64(0)
58 | for i := 0; i < th; i++ {
59 | threadLen := wcGroups[i].Take()
60 | utils.DLogf("Task Thread | time=%d thread=%d speed=%d", t+1, i+1, threadLen)
61 | byteLen += threadLen
62 | }
63 | speed.Speeds[t] = byteLen
64 | speed.TotalSize += byteLen
65 | speed.MaxSpeed = structs.Max(speed.MaxSpeed, byteLen)
66 | }
67 | speed.AvgSpeed = speed.TotalSize / uint64(cfg.DownloadDuration)
68 |
69 | for i := 0; i < th; i++ {
70 | ctxCancels[i]()
71 | }
72 | }
73 |
74 | func SingleThread(downloadFiles []string, proxy interfaces.Vendor, timeoutSeconds int64, wc *WriteCounter) context.CancelFunc {
75 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds+1)*time.Second)
76 | isCancelled := false
77 |
78 | downloadFilesCopy := downloadFiles[:]
79 | fileLen := len(downloadFilesCopy)
80 | readyChan := make(chan bool)
81 |
82 | go func() {
83 | isReady := false
84 | defer func() {
85 | if !isReady {
86 | close(readyChan)
87 | }
88 | }()
89 |
90 | // 100 only for safty
91 | for i := 0; i < 100; i++ {
92 | // if outside cancel or deadline meet(either by time or by hand)
93 | if isCancelled || ctx.Err() != nil {
94 | return
95 | }
96 | // download file
97 | file := downloadFilesCopy[i%fileLen]
98 | resp, _, err := vendors.RequestUnsafe(ctx, proxy, &interfaces.RequestOptions{
99 | URL: file,
100 | })
101 |
102 | if !isReady {
103 | isReady = true
104 | close(readyChan)
105 | }
106 | if err == nil {
107 | var bodyReader io.Reader = nil
108 | if wc.RateLimit >= 1024 {
109 | bucket := ratelimit.NewBucketWithRate(float64(wc.RateLimit)*0.95, wc.RateLimit)
110 | bodyReader = ratelimit.Reader(resp.Body, bucket)
111 | } else {
112 | bodyReader = resp.Body
113 | }
114 |
115 | _, _ = io.Copy(io.Discard, io.TeeReader(bodyReader, wc))
116 | }
117 | // close body
118 | if resp != nil && resp.Body != nil {
119 | _ = resp.Body.Close()
120 | }
121 | }
122 | }()
123 |
124 | <-readyChan
125 | return func() {
126 | isCancelled = true
127 | cancel()
128 | }
129 | }
130 |
131 | func RefetchDownloadFiles(proxy interfaces.Vendor, file string) []string {
132 | defaultList := []string{preconfigs.SPEED_DEFAULT_LARGE_FILE_STATIC_MSFT}
133 | if proxy == nil || proxy.Status() == interfaces.VStatusNotReady {
134 | return defaultList
135 | }
136 |
137 | switch file {
138 | case preconfigs.SPEED_DEFAULT_LARGE_FILE_DYN_INTL:
139 | body, _, _ := vendors.RequestWithRetry(proxy, 1, 1000, &interfaces.RequestOptions{
140 | URL: "https://ipinfo.io",
141 | NoRedir: true,
142 | })
143 |
144 | if strings.Contains(string(body), "Microsoft") {
145 | return []string{preconfigs.SPEED_DEFAULT_LARGE_FILE_STATIC_MSFT}
146 | } else {
147 | return []string{preconfigs.SPEED_DEFAULT_LARGE_FILE_STATIC_GOOGLE}
148 | }
149 | case preconfigs.SPEED_DEFAULT_LARGE_FILE_DYN_FAST:
150 | body, _, _ := vendors.RequestWithRetry(proxy, 3, 1000, &interfaces.RequestOptions{
151 | URL: "https://api.fast.com/netflix/speedtest/v2?https=false&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5",
152 | NoRedir: true,
153 | })
154 | url := jsoniter.Get(body, "targets", 0, "url").ToString()
155 | if url != "" {
156 | return []string{url}
157 | } else {
158 | return defaultList
159 | }
160 | }
161 | return []string{file}
162 | }
163 |
--------------------------------------------------------------------------------
/service/macros/speed/writer.go:
--------------------------------------------------------------------------------
1 | package speed
2 |
3 | import "sync"
4 |
5 | type WriteCounter struct {
6 | Total uint64
7 | RateLimit int64
8 | Lock sync.Mutex
9 | }
10 |
11 | func (wc *WriteCounter) Write(p []byte) (int, error) {
12 | n := len(p)
13 | wc.Lock.Lock()
14 | wc.Total += uint64(n)
15 | wc.Lock.Unlock()
16 | return n, nil
17 | }
18 |
19 | func (wc *WriteCounter) Take() uint64 {
20 | wc.Lock.Lock()
21 | t := wc.Total
22 | wc.Total = 0
23 | wc.Lock.Unlock()
24 | return t
25 | }
26 |
--------------------------------------------------------------------------------
/service/macros/udp/macro.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/airportr/miaospeed/interfaces"
7 | "github.com/airportr/miaospeed/preconfigs"
8 | )
9 |
10 | type Udp struct {
11 | NATType string
12 | }
13 |
14 | func (m *Udp) Type() interfaces.SlaveRequestMacroType {
15 | return interfaces.MacroUDP
16 | }
17 |
18 | func (m *Udp) Run(proxy interfaces.Vendor, r *interfaces.SlaveRequest) error {
19 | stunURL := strings.TrimSpace(r.Configs.STUNURL)
20 | if stunURL == "" {
21 | stunURL = preconfigs.PROXY_DEFAULT_STUN_SERVER
22 | }
23 |
24 | mapType, filterType := detectNATType(proxy, stunURL)
25 | m.NATType = natTypeToString(mapType, filterType)
26 |
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/service/macros/udp/nat.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "time"
7 |
8 | "github.com/airportr/miaospeed/utils"
9 | "github.com/pion/stun"
10 | )
11 |
12 | type NATMapType int
13 |
14 | const (
15 | NATMapFailed NATMapType = iota
16 | NATMapIndependent
17 | NATMapAddrIndependent
18 | NATMapAddrPortIndependent
19 | NATMapNoNat
20 | )
21 |
22 | type NATFilterType int
23 |
24 | const (
25 | NATFilterFailed NATFilterType = iota
26 | NATFilterIndependent
27 | NATFilterAddrIndependent
28 | NATFilterAddrPortIndependent
29 | )
30 |
31 | type stunServerConn struct {
32 | conn net.PacketConn
33 | LocalAddr net.Addr
34 | RemoteAddr *net.UDPAddr
35 | OtherAddr *net.UDPAddr
36 | messageChan chan *stun.Message
37 | }
38 |
39 | func (c *stunServerConn) Close() error {
40 | return nil
41 | }
42 |
43 | const (
44 | messageHeaderSize = 20
45 | natTimeout = 5
46 | )
47 |
48 | var (
49 | errResponseMessage = errors.New("error reading from response message channel")
50 | errTimedOut = errors.New("timed out waiting for response")
51 | errNoOtherAddress = errors.New("no OTHER-ADDRESS in message")
52 | )
53 |
54 | // RFC5780: 4.3. Determining NAT Mapping Behavior
55 | func MappingTests(conn net.PacketConn, addrStr string) NATMapType {
56 | mapTestConn, err := connect(conn, addrStr)
57 | if err != nil {
58 | utils.DLog("NAT MAP TEST | cannot connect to stun server:", err.Error())
59 | return NATMapFailed
60 | }
61 |
62 | // Test I: Regular binding request
63 | request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
64 | resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr, natTimeout)
65 | if err != nil {
66 | utils.DLog("NAT MAP TEST | TEST I Failed:", err.Error())
67 | return NATMapFailed
68 | }
69 |
70 | // Parse response message for XOR-MAPPED-ADDRESS and make sure OTHER-ADDRESS valid
71 | resps1 := parse(resp)
72 | if resps1.xorAddr == nil || resps1.otherAddr == nil {
73 | utils.DLog("NAT MAP TEST | TEST I Failed: no other address")
74 | return NATMapFailed
75 | }
76 | addr, err := net.ResolveUDPAddr("udp4", resps1.otherAddr.String())
77 | if err != nil {
78 | utils.DLog("NAT MAP TEST | TEST I Resolve Failed:", err.Error())
79 | return NATMapFailed
80 | }
81 | mapTestConn.OtherAddr = addr
82 |
83 | // Assert mapping behavior
84 | if resps1.xorAddr.String() == mapTestConn.LocalAddr.String() {
85 | return NATMapNoNat
86 | }
87 |
88 | // Test II: Send binding request to the other address but primary port
89 | oaddr := *mapTestConn.OtherAddr
90 | oaddr.Port = mapTestConn.RemoteAddr.Port
91 | resp, err = mapTestConn.roundTrip(request, &oaddr, natTimeout)
92 | if err != nil {
93 | utils.DLog("NAT MAP TEST | TEST II Failed:", err.Error())
94 | return NATMapFailed
95 | }
96 |
97 | // Assert mapping behavior
98 | resps2 := parse(resp)
99 | if resps2.xorAddr.String() == resps1.xorAddr.String() {
100 | return NATMapIndependent
101 | }
102 |
103 | // Test III: Send binding request to the other address and port
104 | resp, err = mapTestConn.roundTrip(request, mapTestConn.OtherAddr, natTimeout)
105 | if err != nil {
106 | utils.DLog("NAT MAP TEST | TEST III Failed:", err.Error())
107 | return NATMapFailed
108 | }
109 |
110 | // Assert mapping behavior
111 | resps3 := parse(resp)
112 | if resps3.xorAddr.String() == resps2.xorAddr.String() {
113 | return NATMapAddrIndependent
114 | } else {
115 | return NATMapAddrPortIndependent
116 | }
117 | }
118 |
119 | // RFC5780: 4.4. Determining NAT Filtering Behavior
120 | func FilteringTests(conn net.PacketConn, addrStr string) NATFilterType {
121 | mapTestConn, err := connect(conn, addrStr)
122 | if err != nil {
123 | utils.DLog("NAT FLT TEST | cannot connect to stun server:", err.Error())
124 | return NATFilterFailed
125 | }
126 |
127 | // Test I: Regular binding request
128 | request := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
129 | resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr, natTimeout)
130 | if err != nil || errors.Is(err, errTimedOut) {
131 | utils.DLog("NAT FLT TEST | TEST I Failed:", err.Error())
132 | return NATFilterFailed
133 | }
134 | resps := parse(resp)
135 | if resps.xorAddr == nil || resps.otherAddr == nil {
136 | utils.DLog("NAT FLT TEST | TEST I Failed: no other address")
137 | return NATFilterFailed
138 | }
139 | addr, err := net.ResolveUDPAddr("udp4", resps.otherAddr.String())
140 | if err != nil {
141 | utils.DLog("NAT FLT TEST | TEST I Failed:", err.Error())
142 | return NATFilterFailed
143 | }
144 | mapTestConn.OtherAddr = addr
145 |
146 | // Test II: Request to change both IP and port
147 | request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
148 | request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06})
149 |
150 | resp, err = mapTestConn.roundTrip(request, mapTestConn.RemoteAddr, natTimeout)
151 | if err == nil {
152 | return NATFilterIndependent
153 | } else if !errors.Is(err, errTimedOut) {
154 | utils.DLog("NAT FLT TEST | TEST II Failed:", err.Error())
155 | return NATFilterFailed
156 | }
157 |
158 | // Test III: Request to change port only
159 | request = stun.MustBuild(stun.TransactionID, stun.BindingRequest)
160 | request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02})
161 | resp, err = mapTestConn.roundTrip(request, mapTestConn.RemoteAddr, natTimeout)
162 | if err == nil {
163 | return NATFilterAddrIndependent
164 | } else if errors.Is(err, errTimedOut) {
165 | return NATFilterAddrPortIndependent
166 | }
167 |
168 | return NATFilterFailed
169 | }
170 |
171 | // Parse a STUN message
172 | func parse(msg *stun.Message) (ret struct {
173 | xorAddr *stun.XORMappedAddress
174 | otherAddr *stun.OtherAddress
175 | respOrigin *stun.ResponseOrigin
176 | mappedAddr *stun.MappedAddress
177 | software *stun.Software
178 | }) {
179 | ret.mappedAddr = &stun.MappedAddress{}
180 | ret.xorAddr = &stun.XORMappedAddress{}
181 | ret.respOrigin = &stun.ResponseOrigin{}
182 | ret.otherAddr = &stun.OtherAddress{}
183 | ret.software = &stun.Software{}
184 | if ret.xorAddr.GetFrom(msg) != nil {
185 | ret.xorAddr = nil
186 | }
187 | if ret.otherAddr.GetFrom(msg) != nil {
188 | ret.otherAddr = nil
189 | }
190 | if ret.respOrigin.GetFrom(msg) != nil {
191 | ret.respOrigin = nil
192 | }
193 | if ret.mappedAddr.GetFrom(msg) != nil {
194 | ret.mappedAddr = nil
195 | }
196 | if ret.software.GetFrom(msg) != nil {
197 | ret.software = nil
198 | }
199 | return ret
200 | }
201 |
202 | // Given an address string, returns a StunServerConn
203 | func connect(conn net.PacketConn, addrStr string) (*stunServerConn, error) {
204 | addr, err := net.ResolveUDPAddr("udp4", addrStr)
205 | if err != nil {
206 | return nil, err
207 | }
208 |
209 | mChan := listen(conn)
210 | return &stunServerConn{
211 | conn: conn,
212 | LocalAddr: conn.LocalAddr(),
213 | RemoteAddr: addr,
214 | messageChan: mChan,
215 | }, nil
216 | }
217 |
218 | // Send request and wait for response or timeout
219 | func (c *stunServerConn) roundTrip(msg *stun.Message, addr net.Addr, timeout int) (*stun.Message, error) {
220 | _ = msg.NewTransactionID()
221 | _, err := c.conn.WriteTo(msg.Raw, addr)
222 | if err != nil {
223 | return nil, err
224 | }
225 |
226 | // Wait for response or timeout
227 | select {
228 | case m, ok := <-c.messageChan:
229 | if !ok {
230 | return nil, errResponseMessage
231 | }
232 | return m, nil
233 | case <-time.After(time.Duration(timeout) * time.Second):
234 | return nil, errTimedOut
235 | }
236 | }
237 |
238 | // taken from https://github.com/pion/stun/blob/master/cmd/stun-traversal/main.go
239 | func listen(conn net.PacketConn) (messages chan *stun.Message) {
240 | messages = make(chan *stun.Message)
241 | go func() {
242 | for {
243 | buf := make([]byte, 1024)
244 |
245 | n, _, err := conn.ReadFrom(buf)
246 | if err != nil {
247 | close(messages)
248 | return
249 | }
250 | buf = buf[:n]
251 |
252 | m := new(stun.Message)
253 | m.Raw = buf
254 | err = m.Decode()
255 | if err != nil {
256 | close(messages)
257 | return
258 | }
259 |
260 | messages <- m
261 | }
262 | }()
263 | return
264 | }
265 |
--------------------------------------------------------------------------------
/service/macros/udp/udp.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "context"
5 | "strings"
6 | "sync"
7 |
8 | "github.com/airportr/miaospeed/interfaces"
9 | )
10 |
11 | func detectNATType(proxy interfaces.Vendor, url string) (nmt NATMapType, nft NATFilterType) {
12 | addrStr := strings.TrimLeft(url, "udp://")
13 |
14 | wg := sync.WaitGroup{}
15 | ctx := context.Background()
16 |
17 | wg.Add(1)
18 | go func() {
19 | if instance, _ := proxy.DialUDP(ctx, url); instance != nil {
20 | nmt = MappingTests(instance, addrStr)
21 | instance.Close()
22 | }
23 | wg.Done()
24 | }()
25 |
26 | wg.Add(1)
27 | go func() {
28 | if instance, _ := proxy.DialUDP(ctx, url); instance != nil {
29 | nft = FilteringTests(instance, addrStr)
30 | instance.Close()
31 | }
32 | wg.Done()
33 | }()
34 |
35 | wg.Wait()
36 | return
37 | }
38 |
39 | func natTypeToString(nmt NATMapType, nft NATFilterType) string {
40 | if nmt == NATMapFailed || nft == NATFilterFailed {
41 | return "Unknown"
42 | }
43 |
44 | if nmt == NATMapIndependent {
45 | if nft == NATFilterIndependent {
46 | return "FullCone"
47 | } else if nft == NATFilterAddrIndependent {
48 | return "RestrictedCone"
49 | } else {
50 | return "PortRestrictedCone"
51 | }
52 | }
53 |
54 | if nmt == NATMapAddrPortIndependent && nft == NATFilterAddrPortIndependent {
55 | return "Symmetric"
56 | }
57 | return "SymmetricFirewall"
58 | }
59 |
--------------------------------------------------------------------------------
/service/matrices/averagespeed/matrix.go:
--------------------------------------------------------------------------------
1 | package averagespeed
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/speed"
6 | )
7 |
8 | type AverageSpeed struct {
9 | interfaces.AverageSpeedDS
10 | }
11 |
12 | func (m *AverageSpeed) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixAverageSpeed
14 | }
15 |
16 | func (m *AverageSpeed) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroSpeed
18 | }
19 |
20 | func (m *AverageSpeed) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*speed.Speed); ok {
22 | m.Value = mac.AvgSpeed
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/debug/matrix.go:
--------------------------------------------------------------------------------
1 | package debug
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/sleep"
6 | )
7 |
8 | type SleepDS struct {
9 | Value string
10 | }
11 |
12 | func (s *SleepDS) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixSleep
14 | }
15 |
16 | func (s *SleepDS) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroSleep
18 | }
19 |
20 | func (s *SleepDS) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*sleep.Sleep); ok {
22 | s.Value = mac.Value
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/httpping/matrix.go:
--------------------------------------------------------------------------------
1 | package httpping
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type HTTPPing struct {
9 | interfaces.HTTPPingDS
10 | }
11 |
12 | func (m *HTTPPing) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixHTTPPing
14 | }
15 |
16 | func (m *HTTPPing) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *HTTPPing) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Value = mac.Request
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/httpstatuscode/matrix.go:
--------------------------------------------------------------------------------
1 | package httpstatuscode
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type HTTPStatusCode struct {
9 | interfaces.HTTPStatusCodeDS
10 | }
11 |
12 | func (m *HTTPStatusCode) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixHTTPCode
14 | }
15 |
16 | func (m *HTTPStatusCode) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *HTTPStatusCode) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Values = mac.StatusCodes
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/inboundgeoip/matrix.go:
--------------------------------------------------------------------------------
1 | package inboundgeoip
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/geo"
6 | )
7 |
8 | type InboundGeoIP struct {
9 | interfaces.InboundGeoIPDS
10 | }
11 |
12 | func (m *InboundGeoIP) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixInboundGeoIP
14 | }
15 |
16 | func (m *InboundGeoIP) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroGeo
18 | }
19 |
20 | func (m *InboundGeoIP) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*geo.Geo); ok {
22 | m.MultiStacks = mac.InStacks
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/invalid/matrix.go:
--------------------------------------------------------------------------------
1 | package invalid
2 |
3 | import "github.com/airportr/miaospeed/interfaces"
4 |
5 | type Invalid struct {
6 | interfaces.InvalidDS
7 | }
8 |
9 | func (m *Invalid) Type() interfaces.SlaveRequestMatrixType {
10 | return interfaces.MatrixInvalid
11 | }
12 |
13 | func (m *Invalid) MacroJob() interfaces.SlaveRequestMacroType {
14 | return interfaces.MacroInvalid
15 | }
16 |
17 | func (m *Invalid) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
18 | }
19 |
--------------------------------------------------------------------------------
/service/matrices/matrices.go:
--------------------------------------------------------------------------------
1 | package matrices
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/matrices/httpstatuscode"
6 | "github.com/airportr/miaospeed/service/matrices/totalrttping"
7 | "github.com/airportr/miaospeed/utils/structs"
8 |
9 | "github.com/airportr/miaospeed/service/matrices/averagespeed"
10 | "github.com/airportr/miaospeed/service/matrices/debug"
11 | "github.com/airportr/miaospeed/service/matrices/httpping"
12 | "github.com/airportr/miaospeed/service/matrices/inboundgeoip"
13 | "github.com/airportr/miaospeed/service/matrices/invalid"
14 | "github.com/airportr/miaospeed/service/matrices/maxrttping"
15 | "github.com/airportr/miaospeed/service/matrices/maxspeed"
16 | "github.com/airportr/miaospeed/service/matrices/outboundgeoip"
17 | "github.com/airportr/miaospeed/service/matrices/packetloss"
18 | "github.com/airportr/miaospeed/service/matrices/persecondspeed"
19 | "github.com/airportr/miaospeed/service/matrices/rttping"
20 | "github.com/airportr/miaospeed/service/matrices/scripttest"
21 | "github.com/airportr/miaospeed/service/matrices/sdhttp"
22 | "github.com/airportr/miaospeed/service/matrices/sdrtt"
23 | "github.com/airportr/miaospeed/service/matrices/udptype"
24 | )
25 |
26 | var registeredList = map[interfaces.SlaveRequestMatrixType]func() interfaces.SlaveRequestMatrix{
27 | interfaces.MatrixHTTPPing: func() interfaces.SlaveRequestMatrix {
28 | return &httpping.HTTPPing{}
29 | },
30 | interfaces.MatrixRTTPing: func() interfaces.SlaveRequestMatrix {
31 | return &rttping.RTTPing{}
32 | },
33 | interfaces.MatrixUDPType: func() interfaces.SlaveRequestMatrix {
34 | return &udptype.UDPType{}
35 | },
36 | interfaces.MatrixAverageSpeed: func() interfaces.SlaveRequestMatrix {
37 | return &averagespeed.AverageSpeed{}
38 | },
39 | interfaces.MatrixMaxSpeed: func() interfaces.SlaveRequestMatrix {
40 | return &maxspeed.MaxSpeed{}
41 | },
42 | interfaces.MatrixPerSecondSpeed: func() interfaces.SlaveRequestMatrix {
43 | return &persecondspeed.PerSecondSpeed{}
44 | },
45 | interfaces.MatrixInboundGeoIP: func() interfaces.SlaveRequestMatrix {
46 | return &inboundgeoip.InboundGeoIP{}
47 | },
48 | interfaces.MatrixOutboundGeoIP: func() interfaces.SlaveRequestMatrix {
49 | return &outboundgeoip.OutboundGeoIP{}
50 | },
51 | interfaces.MatrixScriptTest: func() interfaces.SlaveRequestMatrix {
52 | return &scripttest.ScriptTest{}
53 | },
54 | interfaces.MatrixMAXRTTPing: func() interfaces.SlaveRequestMatrix {
55 | return &maxrttping.MaxRttPing{}
56 | },
57 | interfaces.MatrixSDRTT: func() interfaces.SlaveRequestMatrix { return &sdrtt.SDRTT{} },
58 | interfaces.MatrixSDHTTP: func() interfaces.SlaveRequestMatrix { return &sdhttp.SDHTTP{} },
59 | interfaces.MatrixHTTPCode: func() interfaces.SlaveRequestMatrix { return &httpstatuscode.HTTPStatusCode{} },
60 | interfaces.MatrixTotalRTTPing: func() interfaces.SlaveRequestMatrix { return &totalrttping.TotalRTT{} },
61 | interfaces.MatrixTotalHTTPPing: func() interfaces.SlaveRequestMatrix { return &totalrttping.TotalHTTP{} },
62 | interfaces.MatrixSleep: func() interfaces.SlaveRequestMatrix { return &debug.SleepDS{} },
63 | interfaces.MatrixPacketLoss: func() interfaces.SlaveRequestMatrix { return &packetloss.PacketLoss{} },
64 | }
65 |
66 | func Find(matrixType interfaces.SlaveRequestMatrixType) interfaces.SlaveRequestMatrix {
67 | if newFn, ok := registeredList[matrixType]; ok && newFn != nil {
68 | return newFn()
69 | }
70 |
71 | return &invalid.Invalid{}
72 | }
73 |
74 | func FindBatch(macroTypes []interfaces.SlaveRequestMatrixType) []interfaces.SlaveRequestMatrix {
75 | return structs.Map(macroTypes, func(m interfaces.SlaveRequestMatrixType) interfaces.SlaveRequestMatrix {
76 | return Find(m)
77 | })
78 | }
79 |
80 | func FindBatchFromEntry(macroTypes []interfaces.SlaveRequestMatrixEntry) []interfaces.SlaveRequestMatrix {
81 | return structs.Map(macroTypes, func(m interfaces.SlaveRequestMatrixEntry) interfaces.SlaveRequestMatrix {
82 | return Find(m.Type)
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/service/matrices/maxrttping/matrix.go:
--------------------------------------------------------------------------------
1 | package maxrttping
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type MaxRttPing struct {
9 | interfaces.MaxRTTDS
10 | }
11 |
12 | func (m *MaxRttPing) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixMAXRTTPing
14 | }
15 |
16 | func (m *MaxRttPing) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *MaxRttPing) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Value = mac.MaxRTT
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/maxspeed/matrix.go:
--------------------------------------------------------------------------------
1 | package maxspeed
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/speed"
6 | )
7 |
8 | type MaxSpeed struct {
9 | interfaces.MaxSpeedDS
10 | }
11 |
12 | func (m *MaxSpeed) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixMaxSpeed
14 | }
15 |
16 | func (m *MaxSpeed) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroSpeed
18 | }
19 |
20 | func (m *MaxSpeed) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*speed.Speed); ok {
22 | m.Value = mac.MaxSpeed
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/outboundgeoip/matrix.go:
--------------------------------------------------------------------------------
1 | package outboundgeoip
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/geo"
6 | )
7 |
8 | type OutboundGeoIP struct {
9 | interfaces.OutboundGeoIPDS
10 | }
11 |
12 | func (m *OutboundGeoIP) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixOutboundGeoIP
14 | }
15 |
16 | func (m *OutboundGeoIP) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroGeo
18 | }
19 |
20 | func (m *OutboundGeoIP) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*geo.Geo); ok {
22 | m.MultiStacks = mac.OutStacks
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/packetloss/matrix.go:
--------------------------------------------------------------------------------
1 | package packetloss
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type PacketLoss struct {
9 | interfaces.PacketLossDS
10 | }
11 |
12 | func (m *PacketLoss) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixPacketLoss
14 | }
15 |
16 | func (m *PacketLoss) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *PacketLoss) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Value = mac.PacketLoss
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/persecondspeed/matrix.go:
--------------------------------------------------------------------------------
1 | package persecondspeed
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/speed"
6 | )
7 |
8 | type PerSecondSpeed struct {
9 | interfaces.PerSecondSpeedDS
10 | }
11 |
12 | func (m *PerSecondSpeed) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixPerSecondSpeed
14 | }
15 |
16 | func (m *PerSecondSpeed) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroSpeed
18 | }
19 |
20 | func (m *PerSecondSpeed) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*speed.Speed); ok {
22 | m.Speeds = mac.Speeds[:]
23 | m.Average = mac.AvgSpeed
24 | m.Max = mac.MaxSpeed
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/service/matrices/rttping/matrix.go:
--------------------------------------------------------------------------------
1 | package rttping
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type RTTPing struct {
9 | interfaces.RTTPingDS
10 | }
11 |
12 | func (m *RTTPing) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixRTTPing
14 | }
15 |
16 | func (m *RTTPing) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *RTTPing) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Value = mac.RTT
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/scripttest/matrix.go:
--------------------------------------------------------------------------------
1 | package scripttest
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/script"
6 | )
7 |
8 | type ScriptTest struct {
9 | interfaces.ScriptTestDS
10 | }
11 |
12 | func (m *ScriptTest) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixScriptTest
14 | }
15 |
16 | func (m *ScriptTest) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroScript
18 | }
19 |
20 | func (m *ScriptTest) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*script.Script); ok {
22 | m.Key = entry.Params
23 | m.ScriptResult = mac.Store[entry.Params]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/service/matrices/sdhttp/matrix.go:
--------------------------------------------------------------------------------
1 | package sdhttp
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type SDHTTP struct {
9 | interfaces.SDHTTPDS
10 | }
11 |
12 | func (m *SDHTTP) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixSDHTTP
14 | }
15 |
16 | func (m *SDHTTP) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *SDHTTP) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Value = mac.RequestSD
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/sdrtt/matrix.go:
--------------------------------------------------------------------------------
1 | package sdrtt
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type SDRTT struct {
9 | interfaces.SDRTTDS
10 | }
11 |
12 | func (m *SDRTT) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixSDRTT
14 | }
15 |
16 | func (m *SDRTT) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroPing
18 | }
19 |
20 | func (m *SDRTT) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*ping.Ping); ok {
22 | m.Value = mac.RTTSD
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/matrices/totalrttping/matrix.go:
--------------------------------------------------------------------------------
1 | package totalrttping
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/ping"
6 | )
7 |
8 | type TotalRTT struct {
9 | interfaces.TotalRTTDS
10 | }
11 |
12 | type TotalHTTP struct {
13 | interfaces.TotalHTTPDS
14 | }
15 |
16 | func (m *TotalRTT) Type() interfaces.SlaveRequestMatrixType {
17 | return interfaces.MatrixTotalRTTPing
18 | }
19 |
20 | func (m *TotalRTT) MacroJob() interfaces.SlaveRequestMacroType {
21 | return interfaces.MacroPing
22 | }
23 |
24 | func (m *TotalRTT) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
25 | if mac, ok := macro.(*ping.Ping); ok {
26 | m.Values = mac.RTTList
27 | }
28 | }
29 |
30 | func (m *TotalHTTP) Type() interfaces.SlaveRequestMatrixType {
31 | return interfaces.MatrixTotalHTTPPing
32 | }
33 |
34 | func (m *TotalHTTP) MacroJob() interfaces.SlaveRequestMacroType {
35 | return interfaces.MacroPing
36 | }
37 |
38 | func (m *TotalHTTP) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
39 | if mac, ok := macro.(*ping.Ping); ok {
40 | m.Values = mac.RequestList
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/service/matrices/udptype/matrix.go:
--------------------------------------------------------------------------------
1 | package udptype
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros/udp"
6 | )
7 |
8 | type UDPType struct {
9 | interfaces.UDPTypeDS
10 | }
11 |
12 | func (m *UDPType) Type() interfaces.SlaveRequestMatrixType {
13 | return interfaces.MatrixUDPType
14 | }
15 |
16 | func (m *UDPType) MacroJob() interfaces.SlaveRequestMacroType {
17 | return interfaces.MacroUDP
18 | }
19 |
20 | func (m *UDPType) Extract(entry interfaces.SlaveRequestMatrixEntry, macro interfaces.SlaveRequestMacro) {
21 | if mac, ok := macro.(*udp.Udp); ok {
22 | m.Value = mac.NATType
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/runner.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/service/macros"
6 | "github.com/airportr/miaospeed/utils/structs"
7 | )
8 |
9 | func ExtractMacrosFromMatrices(matrices []interfaces.SlaveRequestMatrix) []interfaces.SlaveRequestMacroType {
10 | macroTypes := structs.NewSet[interfaces.SlaveRequestMacroType]()
11 | for _, matrix := range matrices {
12 | macroTypes.Add(matrix.MacroJob())
13 | }
14 | return structs.Filter(macroTypes.Digest(), func(m interfaces.SlaveRequestMacroType) bool {
15 | return macros.Find(m).Type() != interfaces.MacroInvalid
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/service/server.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | jsoniter "github.com/json-iterator/go"
5 | "io"
6 | "net"
7 | "net/http"
8 | "os"
9 | "strings"
10 |
11 | "github.com/airportr/miaospeed/interfaces"
12 | "github.com/airportr/miaospeed/preconfigs"
13 | "github.com/airportr/miaospeed/utils"
14 | "github.com/airportr/miaospeed/utils/ipfliter"
15 | "github.com/airportr/miaospeed/utils/structs"
16 | "github.com/gorilla/websocket"
17 |
18 | "github.com/airportr/miaospeed/service/matrices"
19 | "github.com/airportr/miaospeed/service/taskpoll"
20 | )
21 |
22 | type WsHandler struct {
23 | Serve func(http.ResponseWriter, *http.Request)
24 | IPFilter *ipfliter.IPFilter
25 | }
26 |
27 | func (wh *WsHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
28 | ip, _, _ := net.SplitHostPort(r.RemoteAddr)
29 | //show simple forbidden text
30 | if !wh.IPFilter.Allowed(ip) {
31 | http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
32 | return
33 | }
34 | if wh.Serve != nil {
35 | wh.Serve(rw, r)
36 | }
37 | }
38 |
39 | var upgrader = websocket.Upgrader{
40 | ReadBufferSize: 1024,
41 | WriteBufferSize: 1024,
42 | }
43 |
44 | func InitServer() {
45 | if utils.GCFG.Binder == "" {
46 | utils.DErrorf("MiaoSpeed Server | Cannot listening the binder, bind=%s", utils.GCFG.Binder)
47 | os.Exit(1)
48 | }
49 |
50 | utils.DWarnf("MiaoSpeed Server | Start Listening, bind=%s", utils.GCFG.Binder)
51 | wsHandler := WsHandler{
52 | IPFilter: ipfliter.New(ipfliter.Options{
53 | AllowedIPs: utils.GCFG.AllowIPs,
54 | BlockByDefault: true,
55 | }),
56 | Serve: func(rw http.ResponseWriter, r *http.Request) {
57 | conn, err := upgrader.Upgrade(rw, r, nil)
58 | if err != nil {
59 | utils.DErrorf("MiaoServer Test | Socket establishing error, error=%s", err.Error())
60 | return
61 | }
62 | defer func() {
63 | _ = conn.Close()
64 | }()
65 | utils.DLogf("MiaoServer | new unverified connection | remote=%s", r.RemoteAddr)
66 | // Verify the websocket path
67 | if !utils.GCFG.ValidateWSPath(r.URL.Path) {
68 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
69 | Error: "invalid websocket path",
70 | })
71 | utils.DWarnf("MiaoServer Test | websocket path error, error=%s", "invalid websocket path")
72 | return
73 | }
74 | var poll *taskpoll.TPController
75 |
76 | batches := structs.NewAsyncMap[string, bool]()
77 | cancel := func() {
78 | if poll != nil {
79 | for id := range batches.ForEach() {
80 | poll.Remove(id, taskpoll.TPExitInterrupt)
81 | }
82 | }
83 | }
84 | defer cancel()
85 | for {
86 | sr := interfaces.SlaveRequest{}
87 | _, r2, err := conn.NextReader()
88 | if err == nil {
89 | // unsafe, ensure jsoniter package receives frequently security updates.
90 | err = jsoniter.NewDecoder(r2).Decode(&sr)
91 | // 原方案
92 | //err = json.NewDecoder(r).Decode(&sr)
93 | if err == io.EOF {
94 | // One value is expected in the message.
95 | err = io.ErrUnexpectedEOF
96 | }
97 | }
98 | if err != nil {
99 | if !strings.Contains(err.Error(), "EOF") && !strings.Contains(err.Error(), "reset by peer") {
100 | utils.DErrorf("MiaoServer Test | Task receiving error, error=%s", err.Error())
101 | }
102 | return
103 | }
104 | verified := utils.GCFG.VerifyRequest(&sr)
105 | utils.DLogf("MiaoServer Test | Receive Task, name=%s invoker=%v verify=%v remote=%s matrices=%v payload=%d", sr.Basics.ID, sr.Basics.Invoker, verified, r.RemoteAddr, sr.Options.Matrices, len(sr.Nodes))
106 |
107 | // verify token
108 | if !verified {
109 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
110 | Error: "cannot verify the request, please check your token",
111 | })
112 | return
113 | }
114 | sr.Challenge = ""
115 |
116 | // verify invoker
117 | if !utils.GCFG.InWhiteList(sr.Basics.Invoker) {
118 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
119 | Error: "the bot id is not in the whitelist",
120 | })
121 | return
122 | }
123 |
124 | // find all matrices
125 | mtrx := matrices.FindBatchFromEntry(sr.Options.Matrices)
126 |
127 | // extra macro from the matrices
128 | macros := ExtractMacrosFromMatrices(mtrx)
129 |
130 | // select poll
131 | if structs.Contains(macros, interfaces.MacroSpeed) || structs.Contains(macros, interfaces.MacroSleep) {
132 | if utils.GCFG.NoSpeedFlag {
133 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
134 | Error: "speedtest is disabled on backend",
135 | })
136 | return
137 | }
138 | poll = SpeedTaskPoll
139 | //awaitingCount := uint(poll.AwaitingCount())
140 | //if awaitingCount > utils.GCFG.TaskLimit {
141 | // _ = conn.WriteJSON(&interfaces.SlaveResponse{
142 | // Error: fmt.Sprintf("too many tasks are waiting, please try later, current queuing=%d", awaitingCount),
143 | // })
144 | // return
145 | //}
146 | } else {
147 | poll = ConnTaskPoll
148 | }
149 |
150 | utils.DLogf("MiaoServer Test | Receive Task, name=%s poll=%s", sr.Basics.ID, poll.Name())
151 |
152 | // build testing item
153 | item := poll.Push((&TestingPollItem{
154 | id: utils.RandomUUID(),
155 | name: sr.Basics.ID,
156 | request: &sr,
157 | matrices: sr.Options.Matrices,
158 | macros: macros,
159 | onProcess: func(self *TestingPollItem, idx int, result interfaces.SlaveEntrySlot) {
160 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
161 | ID: self.ID(),
162 | MiaoSpeedVersion: utils.VERSION,
163 | Progress: &interfaces.SlaveProgress{
164 | Record: result,
165 | Index: idx,
166 | Queuing: poll.AwaitingCount(),
167 | },
168 | })
169 | },
170 | onExit: func(self *TestingPollItem, exitCode taskpoll.TPExitCode) {
171 | batches.Del(self.ID())
172 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
173 | ID: self.ID(),
174 | MiaoSpeedVersion: utils.VERSION,
175 | Result: &interfaces.SlaveTask{
176 | Request: sr,
177 | Results: self.results.ForEach(),
178 | },
179 | })
180 | },
181 | // 计算权重
182 | calcWeight: func(self *TestingPollItem) uint {
183 | return 1
184 | },
185 | }).Init())
186 |
187 | // onstart
188 | if sr.Configs.ApiVersion == 2 {
189 | _ = conn.WriteJSON(&interfaces.SlaveResponse{
190 | ID: item.ID(),
191 | MiaoSpeedVersion: utils.VERSION,
192 | Progress: &interfaces.SlaveProgress{
193 | Queuing: poll.UnsafeAwaitingCount(),
194 | },
195 | })
196 | }
197 | batches.Set(item.ID(), true)
198 | }
199 | },
200 | }
201 |
202 | server := http.Server{
203 | Handler: &wsHandler,
204 | TLSConfig: preconfigs.MakeSelfSignedTLSServer(),
205 | }
206 |
207 | if strings.HasPrefix(utils.GCFG.Binder, "/") {
208 | unixListener, err := net.Listen("unix", utils.GCFG.Binder)
209 | if err != nil {
210 | utils.DErrorf("MiaoServer Launch | Cannot listen on unixsocket %s, error=%s", utils.GCFG.Binder, err.Error())
211 | os.Exit(1)
212 | }
213 | err = server.Serve(unixListener)
214 | if err != nil {
215 | utils.DErrorf("MiaoServer Launch | Cannot serve on unixsocket %s, error=%s", utils.GCFG.Binder, err.Error())
216 | }
217 | } else {
218 | netListener, err := net.Listen("tcp", utils.GCFG.Binder)
219 | if err != nil {
220 | utils.DErrorf("MiaoServer Launch | Cannot listen on socket %s, error=%s", utils.GCFG.Binder, err.Error())
221 | os.Exit(1)
222 | }
223 | if utils.GCFG.MiaoKoSignedTLS {
224 | err := server.ServeTLS(netListener, "", "")
225 | if err != nil {
226 | utils.DErrorf("MiaoServer Launch | Cannot serve on socket %s, error=%s", utils.GCFG.Binder, err.Error())
227 | }
228 | } else {
229 | err := server.Serve(netListener)
230 | if err != nil {
231 | utils.DErrorf("MiaoServer Launch | Cannot serve on socket %s, error=%s", utils.GCFG.Binder, err.Error())
232 | }
233 | }
234 |
235 | }
236 | }
237 |
238 | func CleanUpServer() {
239 | if strings.HasPrefix(utils.GCFG.Binder, "/") {
240 | err := os.Remove(utils.GCFG.Binder)
241 | if err != nil {
242 | utils.DErrorf("MiaoServer CleanUp OS Error | Cannot remove unixsocket %s, error=%s", utils.GCFG.Binder, err.Error())
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/service/service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
--------------------------------------------------------------------------------
/service/task.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/airportr/miaospeed/service/taskpoll"
7 | "github.com/airportr/miaospeed/utils"
8 | )
9 |
10 | var SpeedTaskPoll *taskpoll.TPController
11 | var ConnTaskPoll *taskpoll.TPController
12 |
13 | func StartTaskServer() {
14 | SpeedTaskPoll = taskpoll.NewTaskPollController("SpeedPoll", 1, time.Duration(utils.GCFG.PauseSecond)*time.Second, 200*time.Millisecond)
15 | ConnTaskPoll = taskpoll.NewTaskPollController("ConnPoll", utils.GCFG.ConnTaskTreading, 0, 200*time.Millisecond)
16 |
17 | go SpeedTaskPoll.Start()
18 | go ConnTaskPoll.Start()
19 | }
20 |
--------------------------------------------------------------------------------
/service/taskpoll/controller.go:
--------------------------------------------------------------------------------
1 | package taskpoll
2 |
3 | import (
4 | "math/rand"
5 | "sync"
6 | "sync/atomic"
7 | "time"
8 |
9 | "github.com/airportr/miaospeed/utils"
10 | "github.com/airportr/miaospeed/utils/structs"
11 | )
12 |
13 | type TPExitCode uint
14 |
15 | const (
16 | TPExitSuccess TPExitCode = iota
17 | TPExitError
18 | TPExitInterrupt
19 | )
20 |
21 | type taskPollItemWrapper struct {
22 | TaskPollItem
23 | counter atomic.Int64
24 | exitOnce sync.Once
25 | }
26 |
27 | func (tpw *taskPollItemWrapper) OnExit(exitCode TPExitCode) {
28 | tpw.exitOnce.Do(func() {
29 | tpw.TaskPollItem.OnExit(exitCode)
30 | })
31 | }
32 |
33 | type TPController struct {
34 | name string
35 | concurrency uint
36 | interval time.Duration
37 | emptyWait time.Duration
38 |
39 | taskPoll []*taskPollItemWrapper
40 | runningTask map[string]int
41 |
42 | current atomic.Uint32
43 | pollLock sync.Mutex
44 | }
45 |
46 | func (tpc *TPController) Name() string {
47 | return tpc.name
48 | }
49 |
50 | // single thread
51 | func (tpc *TPController) populate() (int, *taskPollItemWrapper) {
52 | tpc.pollLock.Lock()
53 | defer tpc.pollLock.Unlock()
54 |
55 | if tpc.current.Load() >= uint32(tpc.concurrency) {
56 | return 0, nil
57 | }
58 |
59 | totalWeight := uint(0)
60 | totalCount := 0
61 | for _, tp := range tpc.taskPoll {
62 | totalWeight += tp.Weight()
63 | totalCount += tp.Count()
64 | }
65 |
66 | factor := 0
67 | if totalWeight > 0 {
68 | factor = rand.Intn(int(totalWeight))
69 | }
70 |
71 | for _, tp := range tpc.taskPoll {
72 | factor -= int(tp.Weight())
73 | if factor < 0 {
74 | counter := tp.counter.Load()
75 |
76 | tp.counter.Add(1)
77 | if tp.counter.Load() >= int64(tp.Count()) {
78 | tpc.removeUnsafe(tp.ID(), TPExitSuccess)
79 | }
80 |
81 | tpc.current.Add(1)
82 | tpc.runningTask[tp.ID()] += 1
83 | return int(counter), tp
84 | }
85 | }
86 |
87 | // no task left
88 | time.Sleep(tpc.emptyWait)
89 |
90 | return 0, nil
91 | }
92 |
93 | func (tpc *TPController) release(tpw *taskPollItemWrapper) {
94 | tpc.pollLock.Lock()
95 | defer tpc.pollLock.Unlock()
96 | tpc.runningTask[tpw.ID()] -= 1
97 | inWaitList := structs.MapContains(tpc.taskPoll, func(w *taskPollItemWrapper) string {
98 | return w.ID()
99 | }, tpw.ID())
100 |
101 | if !inWaitList && tpc.runningTask[tpw.ID()] == 0 {
102 | delete(tpc.runningTask, tpw.ID())
103 | tpw.OnExit(TPExitSuccess)
104 | }
105 |
106 | if tpc.current.Load() > 0 {
107 | // atomic
108 | tpc.current.Add(^uint32(0))
109 | }
110 | }
111 |
112 | func (tpc *TPController) AwaitingCount() int {
113 | tpc.pollLock.Lock()
114 | defer tpc.pollLock.Unlock()
115 |
116 | totalCount := 0
117 | for _, tp := range tpc.taskPoll {
118 | totalCount += tp.Count() - int(tp.counter.Load())
119 | }
120 | return totalCount
121 | }
122 |
123 | func (tpc *TPController) UnsafeAwaitingCount() int {
124 | // This function is not concurrency safe and is only used to get a vague estimate of the number of waits
125 | // to get the exact number use AwaitingCount
126 | totalCount := 0
127 | for _, tp := range tpc.taskPoll {
128 | totalCount += tp.Count() - int(tp.counter.Load())
129 | }
130 | return totalCount
131 | }
132 |
133 | func (tpc *TPController) Start() {
134 | sigTerm := utils.MakeSysChan()
135 |
136 | for {
137 | select {
138 | case <-sigTerm:
139 | utils.DLog("task server shutted down.")
140 | return
141 | default:
142 | if itemIdx, tpw := tpc.populate(); tpw != nil {
143 | utils.DLogf("Task Poll | Task Populate, poll=%s type=%s id=%s index=%v", tpc.name, tpw.TaskName(), tpw.ID(), itemIdx)
144 | go func() {
145 | defer func() {
146 | _ = utils.WrapErrorPure("Task population err", recover())
147 | tpc.release(tpw)
148 | }()
149 | tpw.Yield(itemIdx, tpc)
150 | }()
151 | if tpc.interval > 0 {
152 | time.Sleep(tpc.interval)
153 | }
154 | } else {
155 | // extra sleep for over-populated punishment
156 | time.Sleep(40 * time.Millisecond)
157 | }
158 | }
159 | time.Sleep(10 * time.Millisecond)
160 | }
161 | }
162 |
163 | func (tpc *TPController) Push(item TaskPollItem) TaskPollItem {
164 | tpc.pollLock.Lock()
165 | defer tpc.pollLock.Unlock()
166 |
167 | tpc.taskPoll = append(tpc.taskPoll, &taskPollItemWrapper{
168 | TaskPollItem: item,
169 | })
170 |
171 | return item
172 | }
173 |
174 | func (tpc *TPController) removeUnsafe(id string, exitCode TPExitCode) {
175 | var tp *taskPollItemWrapper = nil
176 | tpc.taskPoll = structs.Filter(tpc.taskPoll, func(w *taskPollItemWrapper) bool {
177 | if w.ID() == id {
178 | tp = w
179 | return false
180 | }
181 | return true
182 | })
183 |
184 | if tp != nil && exitCode != TPExitSuccess {
185 | utils.DWarnf("Task Poll | Task interrupted, id=%v reason=%v", id, exitCode)
186 | tp.OnExit(exitCode)
187 | }
188 | }
189 |
190 | func (tpc *TPController) Remove(id string, exitCode TPExitCode) {
191 | tpc.pollLock.Lock()
192 | defer tpc.pollLock.Unlock()
193 |
194 | tpc.removeUnsafe(id, exitCode)
195 | }
196 |
197 | func NewTaskPollController(name string, concurrency uint, interval time.Duration, emptyWait time.Duration) *TPController {
198 | return &TPController{
199 | name: name,
200 | concurrency: structs.WithInDefault(concurrency, 1, 64, 16),
201 | interval: interval,
202 | emptyWait: emptyWait,
203 |
204 | runningTask: make(map[string]int),
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/service/taskpoll/item.go:
--------------------------------------------------------------------------------
1 | package taskpoll
2 |
3 | type TaskPollItem interface {
4 | ID() string
5 | TaskName() string
6 | Weight() uint
7 | Count() int
8 |
9 | Yield(i int, tpc *TPController)
10 | OnExit(exitCode TPExitCode)
11 | Init() TaskPollItem
12 | }
13 |
--------------------------------------------------------------------------------
/service/testingpollitem.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/airportr/miaospeed/interfaces"
8 | "github.com/airportr/miaospeed/service/macros"
9 | "github.com/airportr/miaospeed/service/macros/invalid"
10 | "github.com/airportr/miaospeed/service/matrices"
11 | "github.com/airportr/miaospeed/service/taskpoll"
12 | "github.com/airportr/miaospeed/utils"
13 | "github.com/airportr/miaospeed/utils/structs"
14 | "github.com/airportr/miaospeed/vendors"
15 | )
16 |
17 | type TestingPollItem struct {
18 | id string
19 | name string
20 |
21 | request *interfaces.SlaveRequest
22 | matrices []interfaces.SlaveRequestMatrixEntry
23 | macros []interfaces.SlaveRequestMacroType
24 | results *structs.AsyncArr[interfaces.SlaveEntrySlot]
25 |
26 | onProcess func(self *TestingPollItem, idx int, result interfaces.SlaveEntrySlot)
27 | onExit func(self *TestingPollItem, exitCode taskpoll.TPExitCode)
28 | calcWeight func(self *TestingPollItem) uint
29 |
30 | onProcessLock sync.Mutex
31 | exitOnce sync.Once
32 | }
33 |
34 | func (tpi *TestingPollItem) ID() string {
35 | return tpi.id
36 | }
37 |
38 | func (tpi *TestingPollItem) TaskName() string {
39 | return tpi.name
40 | }
41 |
42 | func (tpi *TestingPollItem) Weight() uint {
43 | // TODO: could arrange weight based on task size
44 | // or customized rules
45 | if tpi.calcWeight != nil {
46 | return tpi.calcWeight(tpi)
47 | }
48 | return 1
49 | }
50 |
51 | func (tpi *TestingPollItem) Count() int {
52 | return len(tpi.request.Nodes)
53 | }
54 |
55 | func (tpi *TestingPollItem) Yield(idx int, tpc *taskpoll.TPController) {
56 | result := interfaces.SlaveEntrySlot{
57 | ProxyInfo: interfaces.ProxyInfo{},
58 | InvokeDuration: -1,
59 | Matrices: []interfaces.MatrixResponse{},
60 | }
61 |
62 | defer func() {
63 | utils.WrapErrorPure("Task yield error", recover())
64 | tpi.results.Push(result)
65 | tpi.onProcessLock.Lock()
66 | defer tpi.onProcessLock.Unlock()
67 | //utils.DWarnf("Task yield idx %d, tpc: %s", idx, tpc.Name())
68 | tpi.onProcess(tpi, idx, result)
69 | }()
70 |
71 | node := tpi.request.Nodes[idx]
72 | vendor := vendors.Find(tpi.request.Vendor).Build(node.Name, node.Payload)
73 | result.ProxyInfo = vendor.ProxyInfo()
74 | macroMap := structs.NewAsyncMap[interfaces.SlaveRequestMacroType, interfaces.SlaveRequestMacro]()
75 |
76 | startTime := time.Now().UnixMilli()
77 | wg := sync.WaitGroup{}
78 | wg.Add(len(tpi.macros))
79 | for _, macro := range tpi.macros {
80 | macroName := macro
81 | go func() {
82 | macro := macros.Find(macroName)
83 | macro.Run(vendor, tpi.request)
84 | macroMap.Set(macroName, macro)
85 | wg.Done()
86 | }()
87 | }
88 | wg.Wait()
89 | endTime := time.Now().UnixMilli()
90 | result.InvokeDuration = endTime - startTime
91 |
92 | result.Matrices = structs.Map(tpi.matrices, func(me interfaces.SlaveRequestMatrixEntry) interfaces.MatrixResponse {
93 | m := matrices.Find(me.Type)
94 | macro := macroMap.MustGet(m.MacroJob())
95 | if macro == nil {
96 | macro = &invalid.Invalid{}
97 | }
98 | m.Extract(me, macro)
99 |
100 | return interfaces.MatrixResponse{
101 | Type: m.Type(),
102 | Payload: utils.ToJSON(m),
103 | }
104 | })
105 | }
106 |
107 | func (tpi *TestingPollItem) OnExit(exitCode taskpoll.TPExitCode) {
108 | tpi.exitOnce.Do(func() {
109 | tpi.onExit(tpi, exitCode)
110 | })
111 | }
112 |
113 | func (tpi *TestingPollItem) Init() taskpoll.TaskPollItem {
114 | tpi.results = structs.NewAsyncArr[interfaces.SlaveEntrySlot]()
115 | return tpi
116 | }
117 |
--------------------------------------------------------------------------------
/utils/archive.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "compress/gzip"
7 | "io"
8 | "path"
9 | "regexp"
10 | )
11 |
12 | func FindAndExtract(gzipReader io.Reader, filters ...regexp.Regexp) (map[string][]byte, error) {
13 | uncompressedStream, err := gzip.NewReader(gzipReader)
14 | if err != nil {
15 | return nil, err
16 | }
17 | defer uncompressedStream.Close()
18 |
19 | tarReader := tar.NewReader(uncompressedStream)
20 | result := map[string][]byte{}
21 |
22 | for {
23 | header, err := tarReader.Next()
24 | if err == io.EOF {
25 | return result, nil
26 | } else if err != nil {
27 | return nil, err
28 | }
29 |
30 | switch header.Typeflag {
31 | case tar.TypeReg:
32 | matched := false
33 | for _, r := range filters {
34 | if r.MatchString(header.Name) {
35 | matched = true
36 | break
37 | }
38 | }
39 |
40 | if matched {
41 | buf := &bytes.Buffer{}
42 | if _, err := io.Copy(buf, tarReader); err == nil {
43 | result[path.Base(header.Name)] = buf.Bytes()
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/utils/challenge.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/sha512"
5 | "encoding/base64"
6 | "strings"
7 |
8 | "github.com/airportr/miaospeed/interfaces"
9 | jsoniter "github.com/json-iterator/go"
10 | )
11 |
12 | // aws-v4-signature-like signing method
13 | func hashMiaoSpeed(token, request string) string {
14 | buildTokens := append([]string{token}, strings.Split(strings.TrimSpace(BUILDTOKEN), "|")...)
15 |
16 | hasher := sha512.New()
17 | hasher.Write([]byte(request))
18 |
19 | for _, t := range buildTokens {
20 | if t == "" {
21 | // unsafe, plase make sure not to let token segment be empty
22 | t = "SOME_TOKEN"
23 | }
24 |
25 | hasher.Write(hasher.Sum([]byte(t)))
26 | }
27 |
28 | return base64.URLEncoding.EncodeToString(hasher.Sum(nil))
29 | }
30 |
31 | //func hashMd5(token string) string {
32 | // hasher := md5.New()
33 | // hasher.Write([]byte(token))
34 | // return hex.EncodeToString(hasher.Sum(nil))
35 | //}
36 |
37 | func SignRequestOld(token string, req *interfaces.SlaveRequestV1) string {
38 | awaitSigned := req.Clone()
39 | awaitSigned.Challenge = ""
40 | if req.RandomSequence == "" {
41 | DWarn("MiaoServer compatibility deprecation: this change will be deprecated in future versions. Please upgrade your client version.")
42 | awaitSigned.Configs.Scripts = make([]interfaces.Script, 0) // fulltclash Premium兼容性修改,fulltclash即将弃用
43 | awaitSigned.Nodes = make([]interfaces.SlaveRequestNode, 0) // 同上
44 | }
45 | awaitSignedStr, _ := jsoniter.MarshalToString(&awaitSigned) //序列化
46 | awaitSignedStr = strings.TrimSpace(awaitSignedStr) //去除多余空格
47 | return hashMiaoSpeed(token, awaitSignedStr)
48 | }
49 | func SignRequest(token string, req *interfaces.SlaveRequest) string {
50 | if req.Configs.ApiVersion == interfaces.ApiV0 || req.Configs.ApiVersion == interfaces.ApiV1 {
51 | return SignRequestOld(token, req.CloneToV1())
52 | } else {
53 | awaitSigned := req.Clone()
54 | awaitSigned.Challenge = ""
55 | awaitSignedStr, _ := jsoniter.MarshalToString(&awaitSigned)
56 | awaitSignedStr = strings.TrimSpace(awaitSignedStr)
57 | return hashMiaoSpeed(token, awaitSignedStr)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/utils/config.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/utils/structs"
6 | )
7 |
8 | type GlobalConfig struct {
9 | Token string
10 | Binder string
11 | WhiteList []string
12 | AllowIPs []string
13 | SpeedLimit uint64
14 | TaskLimit uint
15 | PauseSecond uint
16 | ConnTaskTreading uint
17 | MiaoKoSignedTLS bool
18 | NoSpeedFlag bool
19 | EnableIPv6 bool
20 | MaxmindDB string
21 | Path string
22 | }
23 |
24 | func (gc *GlobalConfig) InWhiteList(invoker string) bool {
25 | if len(gc.WhiteList) == 0 {
26 | return true
27 | }
28 |
29 | return structs.Contains(gc.WhiteList, invoker)
30 | }
31 |
32 | func (gc *GlobalConfig) VerifyRequest(req *interfaces.SlaveRequest) bool {
33 | return req.Challenge == gc.SignRequest(req)
34 | }
35 |
36 | func (gc *GlobalConfig) SignRequest(req *interfaces.SlaveRequest) string {
37 | return SignRequest(gc.Token, req)
38 | }
39 |
40 | func (gc *GlobalConfig) ValidateWSPath(path string) bool {
41 | DBlackholef("Path to the websocket to be validated: %s, Predefined websocket path: %s\n", path, gc.Path)
42 | return path == gc.Path
43 | }
44 |
45 | var GCFG GlobalConfig
46 |
--------------------------------------------------------------------------------
/utils/constants.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import _ "embed"
4 |
5 | var COMPILATIONTIME string
6 | var BUILDCOUNT string
7 | var COMMIT string
8 | var BRAND string
9 | var VERSION = "4.5.0"
10 |
11 | const LOGO string = " __ __ _ ____ _ \n| \\/ (_) __ _ ___/ ___| _ __ ___ ___ __| |\n| |\\/| | |/ _` |/ _ \\___ \\| '_ \\ / _ \\/ _ \\/ _` |\n| | | | | (_| | (_) |__) | |_) | __/ __/ (_| |\n|_| |_|_|\\__,_|\\___/____/| .__/ \\___|\\___|\\__,_|\n |_| "
12 |
13 | //go:embed embeded/BUILDTOKEN.key
14 | var BUILDTOKEN string
15 |
16 | const (
17 | IDENTIFIER = "Speed"
18 | )
19 |
--------------------------------------------------------------------------------
/utils/dns.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "strings"
8 | "time"
9 |
10 | "github.com/airportr/miaospeed/interfaces"
11 | "github.com/airportr/miaospeed/utils/structs/memutils"
12 | "github.com/airportr/miaospeed/utils/structs/obliviousmap"
13 | )
14 |
15 | var DnsCache *obliviousmap.ObliviousMap[*interfaces.IPStacks]
16 |
17 | // queryServer = "8.8.8.8:53"
18 | func DNSLookuper(addr string, queryServers []string) []net.IP {
19 | if len(queryServers) == 0 {
20 | result, _ := net.LookupIP(addr)
21 | return result
22 | }
23 |
24 | ipSets := map[string]net.IP{}
25 | for _, server := range queryServers {
26 | r := &net.Resolver{
27 | PreferGo: true,
28 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
29 | d := net.Dialer{
30 | Timeout: time.Millisecond * time.Duration(3000),
31 | }
32 | return d.DialContext(ctx, network, server)
33 | },
34 | }
35 | addrs, _ := r.LookupIPAddr(context.Background(), addr)
36 | for _, ia := range addrs {
37 | ipSets[ia.IP.String()] = ia.IP
38 | }
39 | }
40 |
41 | ips := make([]net.IP, len(ipSets))
42 | j := 0
43 | for _, ia := range ipSets {
44 | ips[j] = ia
45 | j += 1
46 | }
47 |
48 | return ips
49 | }
50 |
51 | func LookupIPv46(addr string, retry int, queryServers []string) *interfaces.IPStacks {
52 | token := fmt.Sprintf("%v|%v", addr, queryServers)
53 | if r, ok := DnsCache.Get(token); ok && r != nil {
54 | return r
55 | }
56 |
57 | netips := []net.IP{}
58 | for i := 0; i < retry && len(netips) == 0; i += 1 {
59 | netips = DNSLookuper(addr, queryServers)
60 | }
61 | DLogf("DNS Lookup | dns=%v result=%v", queryServers, netips)
62 |
63 | ipstacks := (&interfaces.IPStacks{}).Init()
64 | for _, ip := range netips {
65 | ipStr := ip.String()
66 | if !strings.Contains(ipStr, ":") {
67 | ipstacks.IPv4 = append(ipstacks.IPv4, ipStr)
68 | } else {
69 | ipstacks.IPv6 = append(ipstacks.IPv6, ipStr)
70 | }
71 | }
72 |
73 | if ipstacks.Count() > 0 {
74 | DnsCache.Set(token, ipstacks)
75 | } else {
76 | DWarnf("DNS Resolver | fail to resolve domain=%s", addr)
77 | }
78 | return ipstacks
79 | }
80 |
81 | func init() {
82 | memIPStacks := memutils.MemDriverMemory[*interfaces.IPStacks]{}
83 | memIPStacks.Init()
84 | DnsCache = obliviousmap.NewObliviousMap[*interfaces.IPStacks]("DnsCache/", time.Minute, true, &memIPStacks)
85 | }
86 |
--------------------------------------------------------------------------------
/utils/embeded/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AirportR/miaospeed/8850c39b0ff504bb752d6ffcbfaa1c3bae3c35bc/utils/embeded/.gitkeep
--------------------------------------------------------------------------------
/utils/ipfliter/ipfliter.go:
--------------------------------------------------------------------------------
1 | package ipfliter
2 |
3 | // Author: https://github.com/jpillora/ipfilter [MIT License]
4 | // Modified by: https://github.com/AirportR [MIT License]
5 | import (
6 | "github.com/tomasen/realip"
7 | "io"
8 | "log"
9 | "net"
10 | "net/http"
11 | "sync"
12 | )
13 |
14 | // Options
15 | //
16 | // for IPFilter. Allow supercedes Block for IP checks
17 | //
18 | // across all matching subnets, whereas country checks use the
19 | // latest Allow/Block setting.
20 | // IPs can be IPv4 or IPv6 and can optionally contain subnet
21 | // masks (e.g. /24). Note however, determining if a given IP is
22 | // included in a subnet requires a linear scan so is less performant
23 | // than looking up single IPs.
24 | //
25 | // This could be improved with cidr range prefix tree.
26 | type Options struct {
27 | //explicity allowed IPs
28 | AllowedIPs []string
29 | //explicity blocked IPs
30 | BlockedIPs []string
31 | //explicity allowed country ISO codes
32 | AllowedCountries []string
33 | //explicity blocked country ISO codes
34 | BlockedCountries []string
35 | //block by default (defaults to allow)
36 | BlockByDefault bool
37 | // TrustProxy enable check request IP from proxy
38 | TrustProxy bool
39 | // Logger enables logging, printing using the provided interface
40 | Logger interface {
41 | Printf(format string, v ...interface{})
42 | }
43 | // These fields currently have no effect
44 | IPDB []byte
45 | IPDBPath string
46 | IPDBNoFetch bool
47 | IPDBFetchURL string
48 | }
49 |
50 | type IPFilter struct {
51 | opts Options
52 | //mut protects the below
53 | //rw since writes are rare
54 | mut sync.RWMutex
55 | defaultAllowed bool
56 | ips map[string]bool
57 | codes map[string]bool
58 | subnets []*subnet
59 | }
60 |
61 | type subnet struct {
62 | str string
63 | ipnet *net.IPNet
64 | allowed bool
65 | }
66 |
67 | // New constructs IPFilter instance without downloading DB.
68 | func New(opts Options) *IPFilter {
69 | if opts.Logger == nil {
70 | //disable logging by default
71 | opts.Logger = log.New(io.Discard, "", 0)
72 | }
73 | f := &IPFilter{
74 | opts: opts,
75 | ips: map[string]bool{},
76 | codes: map[string]bool{},
77 | defaultAllowed: !opts.BlockByDefault,
78 | }
79 | for _, ip := range opts.BlockedIPs {
80 | f.BlockIP(ip)
81 | }
82 | for _, ip := range opts.AllowedIPs {
83 | f.AllowIP(ip)
84 | }
85 | for _, code := range opts.BlockedCountries {
86 | f.BlockCountry(code)
87 | }
88 | for _, code := range opts.AllowedCountries {
89 | f.AllowCountry(code)
90 | }
91 | return f
92 | }
93 |
94 | func (f *IPFilter) printf(format string, args ...interface{}) {
95 | if l := f.opts.Logger; l != nil {
96 | l.Printf("[ipfilter] "+format, args...)
97 | }
98 | }
99 |
100 | func (f *IPFilter) AllowIP(ip string) bool {
101 | return f.ToggleIP(ip, true)
102 | }
103 |
104 | func (f *IPFilter) BlockIP(ip string) bool {
105 | return f.ToggleIP(ip, false)
106 | }
107 |
108 | func (f *IPFilter) ToggleIP(str string, allowed bool) bool {
109 | //check if has subnet
110 | if ip, _net, err := net.ParseCIDR(str); err == nil {
111 | // containing only one ip? (no bits masked)
112 | if n, total := _net.Mask.Size(); n == total {
113 | f.mut.Lock()
114 | f.ips[ip.String()] = allowed
115 | f.mut.Unlock()
116 | return true
117 | }
118 | //check for existing
119 | f.mut.Lock()
120 | found := false
121 | for _, subnet := range f.subnets {
122 | if subnet.str == str {
123 | found = true
124 | subnet.allowed = allowed
125 | break
126 | }
127 | }
128 | if !found {
129 | f.subnets = append(f.subnets, &subnet{
130 | str: str,
131 | ipnet: _net,
132 | allowed: allowed,
133 | })
134 | }
135 | f.mut.Unlock()
136 | return true
137 | }
138 | //check if plain ip (/32)
139 | if ip := net.ParseIP(str); ip != nil {
140 | f.mut.Lock()
141 | f.ips[ip.String()] = allowed
142 | f.mut.Unlock()
143 | return true
144 | }
145 | return false
146 | }
147 |
148 | func (f *IPFilter) AllowCountry(code string) {
149 | f.ToggleCountry(code, true)
150 | }
151 |
152 | func (f *IPFilter) BlockCountry(code string) {
153 | f.ToggleCountry(code, false)
154 | }
155 |
156 | // ToggleCountry alters a specific country setting
157 | func (f *IPFilter) ToggleCountry(code string, allowed bool) {
158 |
159 | f.mut.Lock()
160 | f.codes[code] = allowed
161 | f.mut.Unlock()
162 | }
163 |
164 | // ToggleDefault alters the default setting
165 | func (f *IPFilter) ToggleDefault(allowed bool) {
166 | f.mut.Lock()
167 | f.defaultAllowed = allowed
168 | f.mut.Unlock()
169 | }
170 |
171 | // Allowed returns if a given IP can pass through the filter
172 | func (f *IPFilter) Allowed(ipstr string) bool {
173 | return f.NetAllowed(net.ParseIP(ipstr))
174 | }
175 |
176 | // NetAllowed returns if a given net.IP can pass through the filter
177 | func (f *IPFilter) NetAllowed(ip net.IP) bool {
178 | //invalid ip
179 | if ip == nil {
180 | return false
181 | }
182 | //read lock entire function
183 | //except for db access
184 | f.mut.RLock()
185 | defer f.mut.RUnlock()
186 | //check single ips
187 | allowed, ok := f.ips[ip.String()]
188 | if ok {
189 | return allowed
190 | }
191 | //scan subnets for any allow/block
192 | blocked := false
193 | for _, subnet := range f.subnets {
194 | if subnet.ipnet.Contains(ip) {
195 | if subnet.allowed {
196 | return true
197 | }
198 | blocked = true
199 | }
200 | }
201 | if blocked {
202 | return false
203 | }
204 | //check country codes
205 | //code := NetIPToCountry(ip)
206 | //if code != "" {
207 | // if allowed, ok := f.codes[code]; ok {
208 | // return allowed
209 | // }
210 | //}
211 | //use default setting
212 | return f.defaultAllowed
213 | }
214 |
215 | // Blocked returns if a given IP can NOT pass through the filter
216 | func (f *IPFilter) Blocked(ip string) bool {
217 | return !f.Allowed(ip)
218 | }
219 |
220 | // NetBlocked returns if a given net.IP can NOT pass through the filter
221 | func (f *IPFilter) NetBlocked(ip net.IP) bool {
222 | return !f.NetAllowed(ip)
223 | }
224 |
225 | // Wrap the provided handler with simple IP blocking middleware
226 | // using this IP filter and its configuration
227 | func (f *IPFilter) Wrap(next http.Handler) http.Handler {
228 | return &ipFilterMiddleware{IPFilter: f, next: next}
229 | }
230 |
231 | // Wrap is equivalent to NewLazy(opts) then Wrap(next)
232 | func Wrap(next http.Handler, opts Options) http.Handler {
233 | return New(opts).Wrap(next)
234 | }
235 |
236 | // IPToCountry is a simple IP-country code lookup.
237 | // Returns an empty string when cannot determine country.
238 | //func IPToCountry(ipstr string) string {
239 | // return NetIPToCountry(net.ParseIP(ipstr))
240 | //}
241 |
242 | // NetIPToCountry is a simple IP-country code lookup.
243 | // Returns an empty string when cannot determine country.
244 | //func NetIPToCountry(ip net.IP) string {
245 | // if ip != nil {
246 | // return string(iploc.Country(ip))
247 | // }
248 | // return ""
249 | //}
250 |
251 | type ipFilterMiddleware struct {
252 | *IPFilter
253 | next http.Handler
254 | }
255 |
256 | func (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
257 | var remoteIP string
258 | if m.opts.TrustProxy {
259 | remoteIP = realip.FromRequest(r)
260 | } else {
261 | remoteIP, _, _ = net.SplitHostPort(r.RemoteAddr)
262 | }
263 | allowed := m.IPFilter.Allowed(remoteIP)
264 | //special case localhost ipv4
265 | if !allowed && remoteIP == "::1" && m.IPFilter.Allowed("127.0.0.1") {
266 | allowed = true
267 | }
268 | if !allowed {
269 | //show simple forbidden text
270 | m.printf("blocked %s", remoteIP)
271 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
272 | return
273 | }
274 | //success!
275 | m.next.ServeHTTP(w, r)
276 | }
277 |
278 | // NewNoDB is the same as New
279 | func NewNoDB(opts Options) *IPFilter {
280 | return New(opts)
281 | }
282 |
283 | // NewLazy is the same as New
284 | func NewLazy(opts Options) *IPFilter {
285 | return New(opts)
286 | }
287 |
288 | //func (f *IPFilter) IPToCountry(ipstr string) string {
289 | // return IPToCountry(ipstr)
290 | //}
291 |
292 | //func (f *IPFilter) NetIPToCountry(ip net.IP) string {
293 | // return NetIPToCountry(ip)
294 | //}
295 |
--------------------------------------------------------------------------------
/utils/logger.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "strings"
8 | "time"
9 | )
10 |
11 | type LogType int
12 |
13 | const (
14 | LTDebug LogType = iota
15 | LTLog
16 | LTInfo
17 | LTWarn
18 | LTError
19 | )
20 |
21 | var VerboseLevel = LTWarn
22 |
23 | type LogUnit struct {
24 | Type LogType
25 | Data string
26 | Time int64
27 | TimeStr string
28 | }
29 |
30 | func (l *LogUnit) Error() error {
31 | return errors.New(l.Data)
32 | }
33 |
34 | func LogTypeToStr(lt LogType) string {
35 | if lt == LTLog {
36 | return "ALOG"
37 | } else if lt == LTInfo {
38 | return "INFO"
39 | } else if lt == LTWarn {
40 | return "WARN"
41 | } else if lt == LTError {
42 | return "ERRO"
43 | }
44 | return "UDEF"
45 | }
46 |
47 | func PrintLogUnit(lu *LogUnit) {
48 | if lu.Type < VerboseLevel {
49 | return
50 | }
51 | if lu.Type <= LTWarn {
52 | fmt.Fprintf(os.Stdout, "%s | %s | %s", LogTypeToStr(lu.Type), lu.TimeStr, lu.Data)
53 | } else {
54 | fmt.Fprintf(os.Stderr, "%s | %s | %s", LogTypeToStr(lu.Type), lu.TimeStr, lu.Data)
55 | }
56 | }
57 |
58 | func DBase(t LogType, a ...interface{}) *LogUnit {
59 | currentTime := time.Now()
60 | data := fmt.Sprintln(a...)
61 | log := LogUnit{
62 | Time: currentTime.UnixNano(),
63 | TimeStr: currentTime.Format(time.RFC3339),
64 | Data: data,
65 | Type: t,
66 | }
67 | PrintLogUnit(&log)
68 | return &log
69 | }
70 |
71 | func DBasef(t LogType, format string, a ...interface{}) *LogUnit {
72 | return DBase(t, fmt.Sprintf(format, a...))
73 | }
74 |
75 | func DLog(a ...interface{}) *LogUnit {
76 | return DBase(LTLog, a...)
77 | }
78 |
79 | func DLogf(format string, a ...interface{}) *LogUnit {
80 | return DBasef(LTLog, format, a...)
81 | }
82 |
83 | func DInfo(a ...interface{}) *LogUnit {
84 | return DBase(LTInfo, a...)
85 | }
86 |
87 | func DInfof(format string, a ...interface{}) *LogUnit {
88 | return DBasef(LTInfo, format, a...)
89 | }
90 |
91 | func DWarn(a ...interface{}) *LogUnit {
92 | return DBase(LTWarn, a...)
93 | }
94 |
95 | func DWarnf(format string, a ...interface{}) *LogUnit {
96 | return DBasef(LTWarn, format, a...)
97 | }
98 |
99 | func DError(a ...interface{}) *LogUnit {
100 | return DBase(LTError, a...)
101 | }
102 |
103 | func DErrorf(format string, a ...interface{}) *LogUnit {
104 | return DBasef(LTError, format, a...)
105 | }
106 |
107 | func DBlackhole(a ...interface{}) *LogUnit {
108 | return nil
109 | }
110 |
111 | func DBlackholef(format string, a ...interface{}) *LogUnit {
112 | return nil
113 | }
114 |
115 | func DErrorE(err error, a ...interface{}) *LogUnit {
116 | if err != nil {
117 | a = append(a, err.Error())
118 | }
119 | return DBase(LTError, a...)
120 | }
121 |
122 | func DErrorEf(err error, format string, a ...interface{}) *LogUnit {
123 | if err != nil {
124 | format = strings.TrimSpace(format) + ", error=%s"
125 | a = append(a, err.Error())
126 | }
127 | return DBasef(LTError, format, a...)
128 | }
129 |
130 | func WrapErrorPure(desc string, erro any) (err error) {
131 | if erro != nil {
132 | switch x := erro.(type) {
133 | case string:
134 | err = fmt.Errorf(x)
135 | case error:
136 | err = x
137 | default:
138 | err = fmt.Errorf("unknown error")
139 | }
140 | }
141 |
142 | if err != nil {
143 | DErrorEf(err, "Unexpected Error | %v", desc)
144 | }
145 |
146 | return
147 | }
148 |
149 | func WrapError(desc string, fn func() error, onError ...func(err error)) (err error) {
150 | defer func() {
151 | newErr := WrapErrorPure(desc, recover())
152 | if newErr != nil {
153 | err = newErr
154 | }
155 | if err != nil {
156 | for _, errFn := range onError {
157 | errFn(err)
158 | }
159 | }
160 | }()
161 |
162 | err = fn()
163 | return
164 | }
165 |
--------------------------------------------------------------------------------
/utils/maxmind.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "net"
5 | "strings"
6 |
7 | "github.com/oschwald/maxminddb-golang"
8 | )
9 |
10 | type MMDBResult struct {
11 | IP string
12 | Reverse string
13 | Via string
14 |
15 | ASN int `maxminddb:"autonomous_system_number"`
16 | ASNOrg string `maxminddb:"autonomous_system_organization"`
17 |
18 | City struct {
19 | GeoNameID int `maxminddb:"geoname_id"`
20 | Names struct {
21 | EN string `maxminddb:"en"`
22 | JA string `maxminddb:"ja"`
23 | ZH string `maxminddb:"zh-CN"`
24 | } `maxminddb:"names"`
25 | } `maxminddb:"city"`
26 |
27 | Continent struct {
28 | Code string `maxminddb:"code"`
29 | GeoNameID int `maxminddb:"geoname_id"`
30 | Names struct {
31 | EN string `maxminddb:"en"`
32 | JA string `maxminddb:"ja"`
33 | ZH string `maxminddb:"zh-CN"`
34 | } `maxminddb:"names"`
35 | } `maxminddb:"continent"`
36 |
37 | Country struct {
38 | ISOCode string `maxminddb:"iso_code"`
39 | GeoNameID int `maxminddb:"geoname_id"`
40 | Names struct {
41 | EN string `maxminddb:"en"`
42 | JA string `maxminddb:"ja"`
43 | ZH string `maxminddb:"zh-CN"`
44 | } `maxminddb:"names"`
45 | } `maxminddb:"country"`
46 |
47 | RegisteredCountry struct {
48 | ISOCode string `maxminddb:"iso_code"`
49 | GeoNameID int `maxminddb:"geoname_id"`
50 | Names struct {
51 | EN string `maxminddb:"en"`
52 | JA string `maxminddb:"ja"`
53 | ZH string `maxminddb:"zh-CN"`
54 | } `maxminddb:"names"`
55 | } `maxminddb:"registered_country"`
56 |
57 | Location struct {
58 | Accuracy int `maxminddb:"accuracy_radius"`
59 | Latitude float32 `maxminddb:"latitude"`
60 | Longitude float32 `maxminddb:"longitude"`
61 | TimeZone string `maxminddb:"time_zone"`
62 | } `maxminddb:"location"`
63 | }
64 |
65 | var MaxMindDBs []*maxminddb.Reader
66 |
67 | func LoadMaxMindDB(pathList string) error {
68 | if pathList == "" {
69 | return nil
70 | }
71 |
72 | MaxMindDBs = []*maxminddb.Reader{}
73 | for _, path := range strings.Split(pathList, ",") {
74 | DWarnf("Maxmind Database | Loading maxmind database, path=%v", path)
75 | mmdb, err := maxminddb.Open(path)
76 | if err != nil {
77 | return DErrorf("Maxmind Database | Cannot load maxmind database, err=%v", err.Error()).Error()
78 | }
79 | MaxMindDBs = append(MaxMindDBs, mmdb)
80 | }
81 |
82 | return nil
83 | }
84 |
85 | func QueryMaxMindDB(rawIp string) *MMDBResult {
86 | if len(MaxMindDBs) == 0 {
87 | return nil
88 | }
89 |
90 | result := MMDBResult{
91 | IP: rawIp,
92 | }
93 |
94 | ip := net.ParseIP(rawIp)
95 | if ip == nil {
96 | DErrorf("Maxmind Database | Cannot parse ip address, ip=%v", rawIp)
97 | return &result
98 | }
99 |
100 | for _, db := range MaxMindDBs {
101 | err := db.Lookup(ip, &result)
102 | if err != nil {
103 | DErrorf("Maxmind Database | Cannot query mmdb table, ip=%v err=%v", rawIp, err.Error())
104 | }
105 | }
106 |
107 | return &result
108 | }
109 |
--------------------------------------------------------------------------------
/utils/network.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | )
7 |
8 | func Download(url string) (*http.Response, error) {
9 | req, err := http.NewRequest("GET", url, nil)
10 | if err != nil {
11 | return nil, err
12 | }
13 |
14 | req.Header.Set("User-Agent", "curl/7.73.0 miaospeed/"+VERSION)
15 | resp, err := http.DefaultClient.Do(req)
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | return resp, nil
21 | }
22 |
23 | func DownloadBytes(url string) ([]byte, error) {
24 | resp, err := Download(url)
25 | if err != nil {
26 | return nil, err
27 | }
28 | defer resp.Body.Close()
29 |
30 | body, err := io.ReadAll(resp.Body)
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | return body, nil
36 | }
37 |
--------------------------------------------------------------------------------
/utils/stats.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "math"
4 |
5 | // 计算均值
6 | func mean(data []float64) float64 {
7 | sum := 0.0
8 | for _, value := range data {
9 | sum += value
10 | }
11 | return sum / float64(len(data))
12 | }
13 |
14 | // 计算标准差
15 | func StandardDeviation(data []float64) float64 {
16 | n := len(data)
17 |
18 | if n < 1 {
19 | return 0 // 标准差需要至少一个数据点
20 | }
21 |
22 | // 计算平均值
23 | m := mean(data)
24 |
25 | // 计算方差
26 | variance := 0.0
27 | for _, value := range data {
28 | variance += (value - m) * (value - m)
29 | }
30 | variance /= float64(n) // 使用 n 作为分母
31 |
32 | // 计算标准差
33 | return math.Sqrt(variance)
34 | }
35 |
--------------------------------------------------------------------------------
/utils/structs/asyncarr.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import "sync"
4 |
5 | type AsyncArr[T any] struct {
6 | lock sync.Mutex
7 | store []T
8 | }
9 |
10 | func NewAsyncArr[T any]() *AsyncArr[T] {
11 | return &AsyncArr[T]{
12 | store: make([]T, 0),
13 | }
14 | }
15 |
16 | func (aa *AsyncArr[T]) ForEach() []T {
17 | aa.lock.Lock()
18 | defer aa.lock.Unlock()
19 |
20 | return aa.store[:]
21 | }
22 |
23 | func (aa *AsyncArr[T]) Get(idx int) (*T, bool) {
24 | aa.lock.Lock()
25 | defer aa.lock.Unlock()
26 |
27 | if idx >= 0 && idx < len(aa.store) {
28 | return &aa.store[idx], true
29 | }
30 | return nil, false
31 | }
32 |
33 | func (aa *AsyncArr[T]) MustGet(idx int) *T {
34 | t, _ := aa.Get(idx)
35 | return t
36 | }
37 |
38 | func (aa *AsyncArr[T]) Set(idx int, val T) bool {
39 | aa.lock.Lock()
40 | defer aa.lock.Unlock()
41 |
42 | if idx >= 0 && idx < len(aa.store) {
43 | aa.store[idx] = val
44 | return true
45 | }
46 | return false
47 | }
48 |
49 | func (aa *AsyncArr[T]) Push(val T) {
50 | aa.lock.Lock()
51 | defer aa.lock.Unlock()
52 |
53 | aa.store = append(aa.store, val)
54 | }
55 |
56 | func (aa *AsyncArr[T]) Len() int {
57 | if aa == nil || aa.store == nil {
58 | return 0
59 | }
60 | aa.lock.Lock()
61 | defer aa.lock.Unlock()
62 | return len(aa.store)
63 | }
64 |
65 | func (aa *AsyncArr[T]) Del(idx int) *T {
66 | aa.lock.Lock()
67 | defer aa.lock.Unlock()
68 |
69 | if idx >= 0 && idx < len(aa.store) {
70 | del := aa.store[idx]
71 | aa.store = append(aa.store[:idx], aa.store[idx+1:]...)
72 | return &del
73 | }
74 |
75 | return nil
76 | }
77 |
78 | func (aa *AsyncArr[T]) Take(idx int) *T {
79 | return aa.Del(idx)
80 | }
81 |
--------------------------------------------------------------------------------
/utils/structs/asyncmap.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import "sync"
4 |
5 | type AsyncMap[K Hashable, V any] struct {
6 | lock sync.Mutex
7 | store map[K]V
8 | }
9 |
10 | func NewAsyncMap[K Hashable, T any]() *AsyncMap[K, T] {
11 | return &AsyncMap[K, T]{
12 | store: make(map[K]T),
13 | }
14 | }
15 |
16 | func (am *AsyncMap[K, T]) ForEach() map[K]T {
17 | am.lock.Lock()
18 | defer am.lock.Unlock()
19 |
20 | clones := make(map[K]T)
21 | for k, v := range am.store {
22 | clones[k] = v
23 | }
24 |
25 | return clones
26 | }
27 |
28 | func (am *AsyncMap[K, T]) Get(key K) (T, bool) {
29 | am.lock.Lock()
30 | defer am.lock.Unlock()
31 |
32 | val, ok := am.store[key]
33 | return val, ok
34 | }
35 |
36 | func (am *AsyncMap[K, T]) MustGet(key K) T {
37 | am.lock.Lock()
38 | defer am.lock.Unlock()
39 |
40 | return am.store[key]
41 | }
42 |
43 | func (am *AsyncMap[K, T]) Set(key K, val T) {
44 | am.lock.Lock()
45 | defer am.lock.Unlock()
46 |
47 | am.store[key] = val
48 | }
49 |
50 | func (am *AsyncMap[K, T]) Del(key K) {
51 | am.lock.Lock()
52 | defer am.lock.Unlock()
53 |
54 | delete(am.store, key)
55 | }
56 |
57 | func (am *AsyncMap[K, T]) Take(key K) (T, bool) {
58 | am.lock.Lock()
59 | defer am.lock.Unlock()
60 |
61 | val, ok := am.store[key]
62 | if ok {
63 | delete(am.store, key)
64 | }
65 | return val, ok
66 | }
67 |
--------------------------------------------------------------------------------
/utils/structs/helper.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import "fmt"
4 |
5 | var X = fmt.Sprintf
6 |
7 | func Contains[T comparable](source []T, target T) bool {
8 | for _, s := range source {
9 | if s == target {
10 | return true
11 | }
12 | }
13 | return false
14 | }
15 |
16 | func MapContains[T any, U comparable](source []T, mapper func(T) U, target U) bool {
17 | for _, s := range source {
18 | if mapper(s) == target {
19 | return true
20 | }
21 | }
22 | return false
23 | }
24 |
25 | func Map[T, U any](source []T, mapper func(T) U) []U {
26 | result := make([]U, len(source))
27 |
28 | for i := range source {
29 | result[i] = mapper(source[i])
30 | }
31 |
32 | return result
33 | }
34 |
35 | func Filter[T any](source []T, mapper func(T) bool) []T {
36 | result := make([]T, 0)
37 |
38 | for i := range source {
39 | if mapper(source[i]) {
40 | result = append(result, source[i])
41 | }
42 | }
43 |
44 | return result
45 | }
46 |
47 | func FilterMap[K Hashable, T any](source map[K]T, mapper func(K, T) bool) map[K]T {
48 | result := make(map[K]T)
49 |
50 | for k := range source {
51 | if mapper(k, source[k]) {
52 | result[k] = source[k]
53 | }
54 | }
55 |
56 | return result
57 | }
58 |
59 | func Index[T any](source []T, mapper func(T) bool) int {
60 | for i := range source {
61 | if mapper(source[i]) {
62 | return i
63 | }
64 | }
65 |
66 | return -1
67 | }
68 |
69 | func Exist[T any](source []T, mapper func(T) bool) bool {
70 | for i := range source {
71 | if mapper(source[i]) {
72 | return true
73 | }
74 | }
75 |
76 | return false
77 | }
78 |
79 | func ExistMap[K Hashable, T any](source map[K]T, mapper func(K, T) bool) bool {
80 | for k := range source {
81 | if mapper(k, source[k]) {
82 | return true
83 | }
84 | }
85 |
86 | return false
87 | }
88 |
89 | func MapToArr[K Hashable, T any](source map[K]T) []T {
90 | result := make([]T, 0)
91 |
92 | for k := range source {
93 | result = append(result, source[k])
94 | }
95 |
96 | return result
97 | }
98 |
99 | func MapToArrMap[K Hashable, T, U any](source map[K]T, mapper func(K, T) U) []U {
100 | result := make([]U, 0)
101 |
102 | for k := range source {
103 | result = append(result, mapper(k, source[k]))
104 | }
105 |
106 | return result
107 | }
108 |
109 | func ArrToMap[K Hashable, T, U any](source []T, mapper func(T, int) (K, U)) map[K]U {
110 | result := make(map[K]U)
111 |
112 | for i := range source {
113 | k, v := mapper(source[i], i)
114 | result[k] = v
115 | }
116 |
117 | return result
118 | }
119 |
120 | func Uniq[T any, H Hashable](source []T, mapper func(T) H) []T {
121 | result := make([]T, 0)
122 | set := NewSet[H]()
123 |
124 | for i := range source {
125 | hashKey := mapper(source[i])
126 | if !set.Has(hashKey) {
127 | result = append(result, source[i])
128 | set.Add(hashKey)
129 | }
130 | }
131 |
132 | return result
133 | }
134 |
135 | func Concat[T any](sources ...[]T) []T {
136 | result := make([]T, 0)
137 |
138 | for _, items := range sources {
139 | result = append(result, items...)
140 | }
141 |
142 | return result
143 | }
144 |
145 | func SafeIndex[T any](arr []*T, idx int) *T {
146 | if idx >= len(arr) {
147 | return nil
148 | }
149 | return arr[idx]
150 | }
151 |
152 | func WithIn[T Integer](t, a, b T) T {
153 | if t < a {
154 | return a
155 | } else if b < t {
156 | return b
157 | }
158 | return t
159 | }
160 |
161 | func WithInDefault[T Integer](t, a, b, defaultValue T) T {
162 | if t < a {
163 | return defaultValue
164 | } else if b < t {
165 | return defaultValue
166 | }
167 | return t
168 | }
169 |
170 | func Max[T Integer](a ...T) T {
171 | if len(a) == 0 {
172 | return 0
173 | }
174 |
175 | max := a[0]
176 | for i := 1; i < len(a); i++ {
177 | if a[i] > max {
178 | max = a[i]
179 | }
180 | }
181 |
182 | return max
183 | }
184 |
185 | func Min[T Integer](a ...T) T {
186 | if len(a) == 0 {
187 | return 0
188 | }
189 |
190 | min := a[0]
191 | for i := 1; i < len(a); i++ {
192 | if a[i] < min {
193 | min = a[i]
194 | }
195 | }
196 |
197 | return min
198 | }
199 |
--------------------------------------------------------------------------------
/utils/structs/ipfliter.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | // Author: https://github.com/jpillora/ipfilter [MIT License]
4 | // Modified by: https://github.com/AirportR [MIT License]
5 | import (
6 | "github.com/tomasen/realip"
7 | "io"
8 | "log"
9 | "net"
10 | "net/http"
11 | "sync"
12 | )
13 |
14 | // Options
15 | //
16 | // for IPFilter. Allow supercedes Block for IP checks
17 | //
18 | // across all matching subnets, whereas country checks use the
19 | // latest Allow/Block setting.
20 | // IPs can be IPv4 or IPv6 and can optionally contain subnet
21 | // masks (e.g. /24). Note however, determining if a given IP is
22 | // included in a subnet requires a linear scan so is less performant
23 | // than looking up single IPs.
24 | //
25 | // This could be improved with cidr range prefix tree.
26 | type Options struct {
27 | //explicity allowed IPs
28 | AllowedIPs []string
29 | //explicity blocked IPs
30 | BlockedIPs []string
31 | //explicity allowed country ISO codes
32 | AllowedCountries []string
33 | //explicity blocked country ISO codes
34 | BlockedCountries []string
35 | //block by default (defaults to allow)
36 | BlockByDefault bool
37 | // TrustProxy enable check request IP from proxy
38 | TrustProxy bool
39 | // Logger enables logging, printing using the provided interface
40 | Logger interface {
41 | Printf(format string, v ...interface{})
42 | }
43 | // These fields currently have no effect
44 | IPDB []byte
45 | IPDBPath string
46 | IPDBNoFetch bool
47 | IPDBFetchURL string
48 | }
49 |
50 | type IPFilter struct {
51 | opts Options
52 | //mut protects the below
53 | //rw since writes are rare
54 | mut sync.RWMutex
55 | defaultAllowed bool
56 | ips map[string]bool
57 | codes map[string]bool
58 | subnets []*subnet
59 | }
60 |
61 | type subnet struct {
62 | str string
63 | ipnet *net.IPNet
64 | allowed bool
65 | }
66 |
67 | // New constructs IPFilter instance without downloading DB.
68 | func New(opts Options) *IPFilter {
69 | if opts.Logger == nil {
70 | //disable logging by default
71 | opts.Logger = log.New(io.Discard, "", 0)
72 | }
73 | f := &IPFilter{
74 | opts: opts,
75 | ips: map[string]bool{},
76 | codes: map[string]bool{},
77 | defaultAllowed: !opts.BlockByDefault,
78 | }
79 | for _, ip := range opts.BlockedIPs {
80 | f.BlockIP(ip)
81 | }
82 | for _, ip := range opts.AllowedIPs {
83 | f.AllowIP(ip)
84 | }
85 | for _, code := range opts.BlockedCountries {
86 | f.BlockCountry(code)
87 | }
88 | for _, code := range opts.AllowedCountries {
89 | f.AllowCountry(code)
90 | }
91 | return f
92 | }
93 |
94 | func (f *IPFilter) printf(format string, args ...interface{}) {
95 | if l := f.opts.Logger; l != nil {
96 | l.Printf("[ipfilter] "+format, args...)
97 | }
98 | }
99 |
100 | func (f *IPFilter) AllowIP(ip string) bool {
101 | return f.ToggleIP(ip, true)
102 | }
103 |
104 | func (f *IPFilter) BlockIP(ip string) bool {
105 | return f.ToggleIP(ip, false)
106 | }
107 |
108 | func (f *IPFilter) ToggleIP(str string, allowed bool) bool {
109 | //check if has subnet
110 | if ip, _net, err := net.ParseCIDR(str); err == nil {
111 | // containing only one ip? (no bits masked)
112 | if n, total := _net.Mask.Size(); n == total {
113 | f.mut.Lock()
114 | f.ips[ip.String()] = allowed
115 | f.mut.Unlock()
116 | return true
117 | }
118 | //check for existing
119 | f.mut.Lock()
120 | found := false
121 | for _, subnet := range f.subnets {
122 | if subnet.str == str {
123 | found = true
124 | subnet.allowed = allowed
125 | break
126 | }
127 | }
128 | if !found {
129 | f.subnets = append(f.subnets, &subnet{
130 | str: str,
131 | ipnet: _net,
132 | allowed: allowed,
133 | })
134 | }
135 | f.mut.Unlock()
136 | return true
137 | }
138 | //check if plain ip (/32)
139 | if ip := net.ParseIP(str); ip != nil {
140 | f.mut.Lock()
141 | f.ips[ip.String()] = allowed
142 | f.mut.Unlock()
143 | return true
144 | }
145 | return false
146 | }
147 |
148 | func (f *IPFilter) AllowCountry(code string) {
149 | f.ToggleCountry(code, true)
150 | }
151 |
152 | func (f *IPFilter) BlockCountry(code string) {
153 | f.ToggleCountry(code, false)
154 | }
155 |
156 | // ToggleCountry alters a specific country setting
157 | func (f *IPFilter) ToggleCountry(code string, allowed bool) {
158 |
159 | f.mut.Lock()
160 | f.codes[code] = allowed
161 | f.mut.Unlock()
162 | }
163 |
164 | // ToggleDefault alters the default setting
165 | func (f *IPFilter) ToggleDefault(allowed bool) {
166 | f.mut.Lock()
167 | f.defaultAllowed = allowed
168 | f.mut.Unlock()
169 | }
170 |
171 | // Allowed returns if a given IP can pass through the filter
172 | func (f *IPFilter) Allowed(ipstr string) bool {
173 | return f.NetAllowed(net.ParseIP(ipstr))
174 | }
175 |
176 | // NetAllowed returns if a given net.IP can pass through the filter
177 | func (f *IPFilter) NetAllowed(ip net.IP) bool {
178 | //invalid ip
179 | if ip == nil {
180 | return false
181 | }
182 | //read lock entire function
183 | //except for db access
184 | f.mut.RLock()
185 | defer f.mut.RUnlock()
186 | //check single ips
187 | allowed, ok := f.ips[ip.String()]
188 | if ok {
189 | return allowed
190 | }
191 | //scan subnets for any allow/block
192 | blocked := false
193 | for _, subnet := range f.subnets {
194 | if subnet.ipnet.Contains(ip) {
195 | if subnet.allowed {
196 | return true
197 | }
198 | blocked = true
199 | }
200 | }
201 | if blocked {
202 | return false
203 | }
204 | //check country codes
205 | //code := NetIPToCountry(ip)
206 | //if code != "" {
207 | // if allowed, ok := f.codes[code]; ok {
208 | // return allowed
209 | // }
210 | //}
211 | //use default setting
212 | return f.defaultAllowed
213 | }
214 |
215 | // Blocked returns if a given IP can NOT pass through the filter
216 | func (f *IPFilter) Blocked(ip string) bool {
217 | return !f.Allowed(ip)
218 | }
219 |
220 | // NetBlocked returns if a given net.IP can NOT pass through the filter
221 | func (f *IPFilter) NetBlocked(ip net.IP) bool {
222 | return !f.NetAllowed(ip)
223 | }
224 |
225 | // Wrap the provided handler with simple IP blocking middleware
226 | // using this IP filter and its configuration
227 | func (f *IPFilter) Wrap(next http.Handler) http.Handler {
228 | return &ipFilterMiddleware{IPFilter: f, next: next}
229 | }
230 |
231 | // Wrap is equivalent to NewLazy(opts) then Wrap(next)
232 | func Wrap(next http.Handler, opts Options) http.Handler {
233 | return New(opts).Wrap(next)
234 | }
235 |
236 | // IPToCountry is a simple IP-country code lookup.
237 | // Returns an empty string when cannot determine country.
238 | //func IPToCountry(ipstr string) string {
239 | // return NetIPToCountry(net.ParseIP(ipstr))
240 | //}
241 |
242 | // NetIPToCountry is a simple IP-country code lookup.
243 | // Returns an empty string when cannot determine country.
244 | //func NetIPToCountry(ip net.IP) string {
245 | // if ip != nil {
246 | // return string(iploc.Country(ip))
247 | // }
248 | // return ""
249 | //}
250 |
251 | type ipFilterMiddleware struct {
252 | *IPFilter
253 | next http.Handler
254 | }
255 |
256 | func (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
257 | var remoteIP string
258 | if m.opts.TrustProxy {
259 | remoteIP = realip.FromRequest(r)
260 | } else {
261 | remoteIP, _, _ = net.SplitHostPort(r.RemoteAddr)
262 | }
263 | allowed := m.IPFilter.Allowed(remoteIP)
264 | //special case localhost ipv4
265 | if !allowed && remoteIP == "::1" && m.IPFilter.Allowed("127.0.0.1") {
266 | allowed = true
267 | }
268 | if !allowed {
269 | //show simple forbidden text
270 | m.printf("blocked %s", remoteIP)
271 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
272 | return
273 | }
274 | //success!
275 | m.next.ServeHTTP(w, r)
276 | }
277 |
278 | // NewNoDB is the same as New
279 | func NewNoDB(opts Options) *IPFilter {
280 | return New(opts)
281 | }
282 |
283 | // NewLazy is the same as New
284 | func NewLazy(opts Options) *IPFilter {
285 | return New(opts)
286 | }
287 |
288 | //func (f *IPFilter) IPToCountry(ipstr string) string {
289 | // return IPToCountry(ipstr)
290 | //}
291 |
292 | //func (f *IPFilter) NetIPToCountry(ip net.IP) string {
293 | // return NetIPToCountry(ip)
294 | //}
295 |
--------------------------------------------------------------------------------
/utils/structs/memutils/driver.go:
--------------------------------------------------------------------------------
1 | package memutils
2 |
3 | import "time"
4 |
5 | type MemDriver[T any] interface {
6 | Init(kargs ...string)
7 |
8 | Read(key string) (T, bool)
9 | Write(key string, value T, expire time.Duration, overwriteTTLIfExists bool) T
10 | IncBy(key string, value int, expire time.Duration, overwriteTTLIfExists bool) int
11 | Inc(key string, expire time.Duration, overwriteTTLIfExists bool) int
12 |
13 | List(prefix string) []string
14 | Expire(key string)
15 | SetExpire(key string, duration time.Duration) time.Duration
16 | Exists(key string) bool
17 | Wipe(prefix string)
18 | WipePrefix(prefix string)
19 | }
20 |
21 | func Now() int64 {
22 | return time.Now().UnixNano()
23 | }
24 |
25 | func Zero[T any]() T {
26 | var result T
27 | return result
28 | }
29 |
--------------------------------------------------------------------------------
/utils/structs/memutils/driver_memory.go:
--------------------------------------------------------------------------------
1 | package memutils
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | "time"
7 | )
8 |
9 | type MemDriverMemory[T comparable] struct {
10 | MemDriver[T]
11 |
12 | mem map[string]T
13 | timer map[string]int64
14 |
15 | lock sync.Mutex
16 | }
17 |
18 | func (md *MemDriverMemory[T]) Init(kargs ...string) {
19 | md.mem = make(map[string]T)
20 | md.timer = make(map[string]int64)
21 | }
22 |
23 | func (md *MemDriverMemory[T]) unsafeRead(key string) (T, bool) {
24 | now := Now()
25 | v, ok := md.mem[key]
26 | if ok {
27 | if t, ok := md.timer[key]; ok {
28 | if t <= now {
29 | delete(md.mem, key)
30 | delete(md.timer, key)
31 | return Zero[T](), false
32 | }
33 | return v, true
34 | } else {
35 | delete(md.mem, key)
36 | return Zero[T](), false
37 | }
38 | }
39 | return Zero[T](), false
40 | }
41 |
42 | func (md *MemDriverMemory[T]) Read(key string) (T, bool) {
43 | md.lock.Lock()
44 | defer md.lock.Unlock()
45 |
46 | return md.unsafeRead(key)
47 | }
48 |
49 | func (md *MemDriverMemory[T]) unsafeWrite(key string, value T, expire time.Duration, overwriteTTLIfExists bool) T {
50 | now := Now()
51 | _, ok := md.mem[key]
52 | md.mem[key] = value
53 | if !ok || overwriteTTLIfExists {
54 | md.timer[key] = now + expire.Nanoseconds()
55 | }
56 |
57 | return value
58 | }
59 |
60 | func (md *MemDriverMemory[T]) Write(key string, value T, expire time.Duration, overwriteTTLIfExists bool) T {
61 | md.lock.Lock()
62 | defer md.lock.Unlock()
63 |
64 | return md.unsafeWrite(key, value, expire, overwriteTTLIfExists)
65 | }
66 |
67 | func (md *MemDriverMemory[T]) IncBy(key string, value int, expire time.Duration, overwriteTTLIfExists bool) int {
68 | md.lock.Lock()
69 | defer md.lock.Unlock()
70 |
71 | val, ok := md.unsafeRead(key)
72 | nextVal := value
73 |
74 | if ok {
75 | switch any(val).(type) {
76 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
77 | nextVal += any(val).(int)
78 | default:
79 | return nextVal
80 | }
81 | }
82 |
83 | md.unsafeWrite(key, any(nextVal).(T), expire, overwriteTTLIfExists)
84 | return nextVal
85 | }
86 |
87 | func (md *MemDriverMemory[T]) Inc(key string, expire time.Duration, overwriteTTLIfExists bool) int {
88 | return md.IncBy(key, 1, expire, overwriteTTLIfExists)
89 | }
90 |
91 | func (md *MemDriverMemory[T]) Exists(key string) bool {
92 | md.lock.Lock()
93 | defer md.lock.Unlock()
94 |
95 | now := Now()
96 | if t, ok := md.timer[key]; ok && t > now {
97 | if _, ok := md.mem[key]; ok {
98 | return true
99 | }
100 | }
101 | return false
102 | }
103 |
104 | func (md *MemDriverMemory[T]) Expire(key string) {
105 | md.lock.Lock()
106 | defer md.lock.Unlock()
107 |
108 | _, ok := md.mem[key]
109 | if ok {
110 | delete(md.mem, key)
111 | }
112 | _, ok = md.timer[key]
113 | if ok {
114 | delete(md.timer, key)
115 | }
116 | }
117 |
118 | func (md *MemDriverMemory[T]) SetExpire(key string, duration time.Duration) time.Duration {
119 | md.lock.Lock()
120 | defer md.lock.Unlock()
121 |
122 | if _, ok := md.unsafeRead(key); ok {
123 | md.timer[key] = Now() + duration.Nanoseconds()
124 | }
125 | return duration
126 | }
127 |
128 | func (md *MemDriverMemory[T]) List(key string) []string {
129 | md.lock.Lock()
130 | defer md.lock.Unlock()
131 |
132 | slice := []string{}
133 | now := Now()
134 | for k, v := range md.timer {
135 | if now < v {
136 | slice = append(slice, k)
137 | }
138 | }
139 | return slice
140 | }
141 |
142 | func (md *MemDriverMemory[T]) Wipe(prefix string) {
143 | md.lock.Lock()
144 | defer md.lock.Unlock()
145 |
146 | md.Init()
147 | }
148 |
149 | func (md *MemDriverMemory[T]) WipePrefix(prefix string) {
150 | md.lock.Lock()
151 | defer md.lock.Unlock()
152 |
153 | for k := range md.mem {
154 | if strings.HasPrefix(k, prefix) {
155 | delete(md.mem, k)
156 | delete(md.timer, k)
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/utils/structs/misc.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | type Integer interface {
4 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64
5 | }
6 |
7 | type Ordered interface {
8 | Integer | ~string
9 | }
10 |
11 | type Hashable interface {
12 | Ordered
13 | }
14 |
--------------------------------------------------------------------------------
/utils/structs/obliviousmap/obliviousmap.go:
--------------------------------------------------------------------------------
1 | package obliviousmap
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/airportr/miaospeed/utils/structs/memutils"
8 | )
9 |
10 | type ObliviousMap[T any] struct {
11 | prefix string
12 | driver memutils.MemDriver[T]
13 |
14 | expire time.Duration
15 | hold sync.Mutex
16 | utif bool
17 | }
18 |
19 | func (om *ObliviousMap[T]) Hold(fn func()) {
20 | om.hold.Lock()
21 | defer om.hold.Unlock()
22 |
23 | fn()
24 | }
25 |
26 | func (om *ObliviousMap[T]) Get(key string) (T, bool) {
27 | return om.driver.Read(om.prefix + key)
28 | }
29 |
30 | func (om *ObliviousMap[T]) Set(key string, value T) T {
31 | return om.driver.Write(om.prefix+key, value, om.expire, om.utif)
32 | }
33 |
34 | func (om *ObliviousMap[T]) SetExpire(key string, duration time.Duration) time.Duration {
35 | return om.driver.SetExpire(om.prefix+key, duration)
36 | }
37 |
38 | func (om *ObliviousMap[T]) Unset(key string) {
39 | om.driver.Expire(om.prefix + key)
40 | }
41 |
42 | func (om *ObliviousMap[T]) Exist(key string) bool {
43 | return om.driver.Exists(om.prefix + key)
44 | }
45 |
46 | func (om *ObliviousMap[T]) Wipe() {
47 | om.driver.Wipe(om.prefix)
48 | }
49 |
50 | func (om *ObliviousMap[T]) WipePrefix(prefix string) {
51 | om.driver.WipePrefix(om.prefix + prefix)
52 | }
53 |
54 | func (om *ObliviousMap[T]) AddBy(key string, val int) int {
55 | return om.driver.IncBy(om.prefix+key, val, om.expire, om.utif)
56 | }
57 |
58 | func (om *ObliviousMap[T]) Add(key string) int {
59 | return om.driver.Inc(om.prefix+key, om.expire, om.utif)
60 | }
61 |
62 | func NewObliviousMap[T any](prefix string, expire time.Duration, updateTimeIfWrite bool, driver memutils.MemDriver[T]) *ObliviousMap[T] {
63 | return &ObliviousMap[T]{
64 | prefix: prefix,
65 | expire: expire,
66 | driver: driver,
67 | utif: updateTimeIfWrite,
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/utils/structs/set.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | type Set[T Hashable] struct {
4 | store map[T]bool
5 | }
6 |
7 | func (s *Set[T]) Has(key T) bool {
8 | _, ok := s.store[key]
9 | return ok
10 | }
11 |
12 | func (s *Set[T]) Add(key T) {
13 | s.store[key] = true
14 | }
15 |
16 | func (s *Set[T]) Remove(key T) {
17 | delete(s.store, key)
18 | }
19 |
20 | func (s *Set[T]) Digest() []T {
21 | arr := make([]T, len(s.store))
22 | i := 0
23 | for key := range s.store {
24 | arr[i] = key
25 | i++
26 | }
27 | return arr
28 | }
29 |
30 | func NewSet[T Hashable]() *Set[T] {
31 | return &Set[T]{
32 | store: make(map[T]bool),
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/utils/sys.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "os/signal"
6 | "syscall"
7 | )
8 |
9 | func MakeSysChan() chan os.Signal {
10 | sigCh := make(chan os.Signal, 1)
11 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
12 | return sigCh
13 | }
14 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | jsoniter "github.com/json-iterator/go"
6 | "net"
7 | "os"
8 | "strings"
9 | )
10 |
11 | func RandomUUID() string {
12 | uid, _ := uuid.NewV4()
13 | return uid.String()
14 | }
15 |
16 | func ToJSON(a any) string {
17 | r, _ := jsoniter.MarshalToString(a)
18 | return r
19 | }
20 |
21 | func ReadFile(path string) string {
22 | if path == "" {
23 | return ""
24 | }
25 | file, err := os.ReadFile(path)
26 | if err != nil {
27 | DWarnf("cannot read the file: %s, err: %s", path, err.Error())
28 | return ""
29 | }
30 | return string(file)
31 | }
32 |
33 | func parseIPAddress(input string) (string, error) {
34 | input = strings.Trim(input, "[]")
35 |
36 | host, _, err := net.SplitHostPort(input)
37 | if err != nil {
38 | host = input
39 | }
40 |
41 | ip := net.ParseIP(host)
42 | if ip == nil {
43 | return "", net.InvalidAddrError("invalid IP address")
44 | }
45 |
46 | return ip.String(), nil
47 | }
48 |
--------------------------------------------------------------------------------
/vendors/clash/metadata.go:
--------------------------------------------------------------------------------
1 | package clash
2 |
3 | import (
4 | "fmt"
5 | "net/netip"
6 | "net/url"
7 | "strconv"
8 |
9 | "github.com/metacubex/mihomo/constant"
10 | )
11 |
12 | func urlToMetadata(rawURL string, network constant.NetWork) (addr constant.Metadata, err error) {
13 | u, err := url.Parse(rawURL)
14 | if err != nil {
15 | return
16 | }
17 |
18 | port := u.Port()
19 | if port == "" {
20 | switch u.Scheme {
21 | case "https":
22 | port = "443"
23 | case "http":
24 | port = "80"
25 | default:
26 | err = fmt.Errorf("%s scheme not Support", rawURL)
27 | return
28 | }
29 | }
30 | uintPort, err := strconv.ParseUint(port, 10, 16)
31 | if err != nil {
32 | return
33 | }
34 |
35 | addr = constant.Metadata{
36 | NetWork: network,
37 | Host: u.Hostname(),
38 | DstIP: netip.Addr{},
39 | DstPort: uint16(uintPort),
40 | }
41 | return
42 | }
43 |
44 | //func urlToMetadata(rawURL string, network constant.NetWork) (addr constant.Metadata, err error) {
45 | // u, err := url.Parse(rawURL)
46 | // if err != nil {
47 | // return
48 | // }
49 | //
50 | // port := u.Port()
51 | // if port == "" {
52 | // switch u.Scheme {
53 | // case "https":
54 | // port = "443"
55 | // case "http":
56 | // port = "80"
57 | // default:
58 | // return
59 | // }
60 | // }
61 | // addr = constant.Metadata{
62 | // NetWork: network,
63 | // Host: u.Hostname(),
64 | // DstIP: nil,
65 | // DstPort: port,
66 | // }
67 | // return
68 | //}
69 |
--------------------------------------------------------------------------------
/vendors/clash/profile.go:
--------------------------------------------------------------------------------
1 | package clash
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 | "github.com/airportr/miaospeed/utils"
6 | "github.com/metacubex/mihomo/adapter"
7 | "github.com/metacubex/mihomo/constant"
8 | vendorlog "github.com/sirupsen/logrus"
9 | "gopkg.in/yaml.v2"
10 | )
11 |
12 | func init() {
13 | patch()
14 | }
15 |
16 | // patch is used to fix the logger exit function
17 | func patch() {
18 | logger := vendorlog.StandardLogger()
19 | logger.ExitFunc = func(code int) {}
20 | }
21 | func parseProxy(proxyName, proxyPayload string) constant.Proxy {
22 | var payload map[string]any
23 | yaml.Unmarshal([]byte(proxyPayload), &payload)
24 | proxy, err := adapter.ParseProxy(payload)
25 |
26 | if err != nil {
27 | utils.DLogf("Vendor Parser | Parse clash profile error, error=%v", err.Error())
28 | }
29 |
30 | return proxy
31 | }
32 |
33 | func extractFirstProxy(proxyName, proxyPayload string) constant.Proxy {
34 | proxy := parseProxy(proxyName, proxyPayload)
35 |
36 | if proxy != nil && interfaces.Parse(proxy.Type().String()) != interfaces.ProxyInvalid {
37 | return proxy
38 | }
39 |
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/vendors/clash/vendor.go:
--------------------------------------------------------------------------------
1 | package clash
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/airportr/miaospeed/utils"
7 | "github.com/metacubex/mihomo/component/resolver"
8 | "net"
9 | "strings"
10 | "time"
11 |
12 | "github.com/airportr/miaospeed/interfaces"
13 | "github.com/metacubex/mihomo/constant"
14 | )
15 |
16 | type Clash struct {
17 | proxy constant.Proxy
18 | }
19 |
20 | func setupIPv6() {
21 | if utils.GCFG.EnableIPv6 {
22 | if resolver.DisableIPv6 {
23 | resolver.DisableIPv6 = false
24 | }
25 | }
26 | }
27 |
28 | func (c *Clash) Proxy() constant.Proxy {
29 | return c.proxy
30 | }
31 |
32 | func (c *Clash) Type() interfaces.VendorType {
33 | return interfaces.VendorClash
34 | }
35 |
36 | func (c *Clash) Status() interfaces.VendorStatus {
37 | if c == nil || c.proxy == nil {
38 | return interfaces.VStatusNotReady
39 | }
40 |
41 | return interfaces.VStatusOperational
42 | }
43 |
44 | func (c *Clash) Build(proxyName string, proxyInfo string) interfaces.Vendor {
45 | if c == nil {
46 | c = &Clash{}
47 | }
48 | c.proxy = extractFirstProxy(proxyName, proxyInfo)
49 | return c
50 | }
51 |
52 | func (c *Clash) DialTCP(ctx context.Context, url string, network interfaces.RequestOptionsNetwork) (net.Conn, error) {
53 | if c == nil || c.proxy == nil {
54 | return nil, fmt.Errorf("should call Build() before run")
55 | }
56 | setupIPv6()
57 | timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
58 | defer cancel()
59 | type result struct {
60 | conn net.Conn
61 | err error
62 | }
63 | ch := make(chan result, 1)
64 | go func() {
65 | addr, err := urlToMetadata(url, constant.TCP)
66 | if err != nil {
67 | ch <- result{nil, fmt.Errorf("cannot build tcp context: %v", err)}
68 | }
69 | conn, err := c.proxy.DialContext(timeoutCtx, &addr)
70 | if err != nil && !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "no such host") {
71 | utils.DLogf("cannot dialTCP: %s | proxy=%s | vendor=Clash | err=%s", url, c.proxy.Name(), err.Error())
72 | }
73 | ch <- result{conn, err}
74 | }()
75 | select {
76 | case res := <-ch:
77 | return res.conn, res.err
78 | case <-timeoutCtx.Done():
79 | return nil, fmt.Errorf("dialTCP timeout after 10 seconds: %w", timeoutCtx.Err())
80 | }
81 | }
82 |
83 | func (c *Clash) DialUDP(ctx context.Context, url string) (net.PacketConn, error) {
84 | if c == nil || c.proxy == nil {
85 | return nil, fmt.Errorf("should call Build() before run")
86 | }
87 | setupIPv6()
88 | timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
89 | defer cancel()
90 |
91 | type result struct {
92 | conn net.PacketConn
93 | err error
94 | }
95 | ch := make(chan result, 1)
96 |
97 | go func() {
98 | addr, err := urlToMetadata(url, constant.UDP)
99 | if err != nil {
100 | ch <- result{nil, fmt.Errorf("cannot build udp context: %w", err)}
101 | return
102 | }
103 |
104 | conn, err := c.proxy.ListenPacketContext(timeoutCtx, &addr)
105 | if err != nil && !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "no such host") {
106 | utils.DLogf("cannot dialUDP: %s | proxy=%s | vendor=Clash | err=%s", url, c.proxy.Name(), err.Error())
107 | }
108 | ch <- result{conn, err}
109 | }()
110 |
111 | select {
112 | case res := <-ch:
113 | return res.conn, res.err
114 | case <-timeoutCtx.Done():
115 | return nil, fmt.Errorf("dialUDP timeout after 5 seconds: %w", timeoutCtx.Err())
116 | }
117 | }
118 | func (c *Clash) ProxyInfo() interfaces.ProxyInfo {
119 | if c == nil || c.proxy == nil {
120 | return interfaces.ProxyInfo{}
121 | }
122 |
123 | return interfaces.ProxyInfo{
124 | Name: c.proxy.Name(),
125 | Address: c.proxy.Addr(),
126 | Type: interfaces.Parse(c.proxy.Type().String()),
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/vendors/commons.go:
--------------------------------------------------------------------------------
1 | package vendors
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "net"
11 | "net/http"
12 | "time"
13 |
14 | "github.com/airportr/miaospeed/interfaces"
15 | "github.com/airportr/miaospeed/utils/structs"
16 | )
17 |
18 | // for all methods in commons
19 | // if proxy is nil, fallback to system
20 | // if proxy is not nil but it is not ready
21 | // errors will be returned
22 |
23 | func RequestUnsafe(ctx context.Context, p interfaces.Vendor, reqOpt *interfaces.RequestOptions) (*http.Response, []string, error) {
24 | if p != nil && p.Status() == interfaces.VStatusNotReady || reqOpt == nil {
25 | return nil, nil, errors.New("proxy is not ready")
26 | }
27 |
28 | // check request method
29 | if reqOpt.Method == "" {
30 | reqOpt.Method = http.MethodGet
31 | }
32 |
33 | // check body reader
34 | var reader io.Reader = nil
35 | if len(reqOpt.Body) > 0 {
36 | reader = bytes.NewBuffer(reqOpt.Body)
37 | }
38 |
39 | // build request
40 | req, err := http.NewRequest(reqOpt.Method, reqOpt.URL, reader)
41 | if err != nil {
42 | return nil, nil, err
43 | }
44 |
45 | // write headers and cookies
46 | for hkey, hval := range reqOpt.Headers {
47 | req.Header.Add(hkey, hval)
48 | }
49 | for ckey, cval := range reqOpt.Cookies {
50 | req.AddCookie(&http.Cookie{Name: ckey, Value: cval})
51 | }
52 | req = req.WithContext(ctx)
53 |
54 | // connect proxy bridge
55 | // init params copied from http.DefaultTransport
56 | transport := &http.Transport{
57 | MaxIdleConns: 100,
58 | IdleConnTimeout: 10 * time.Second,
59 | TLSHandshakeTimeout: 5 * time.Second,
60 | ExpectContinueTimeout: 1 * time.Second,
61 | }
62 |
63 | if p != nil {
64 | transport.Dial = func(string, string) (net.Conn, error) {
65 | return p.DialTCP(ctx, reqOpt.URL, reqOpt.Network)
66 | }
67 | } else {
68 | transport.Dial = func(string, string) (net.Conn, error) {
69 | return net.Dial(reqOpt.Network.String(), reqOpt.URL)
70 | }
71 | }
72 |
73 | // make a list to record all redirects
74 | // the maximum count of redirection is 64
75 | redirects := []string{}
76 | client := http.Client{
77 | Timeout: 30 * time.Second,
78 | Transport: transport,
79 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
80 | if reqOpt.NoRedir || len(redirects) > 64 {
81 | return http.ErrUseLastResponse
82 | }
83 |
84 | redirects = append(redirects, req.Response.Header.Get("Location"))
85 | return nil
86 | },
87 | }
88 |
89 | // send the request
90 | resp, err := client.Do(req)
91 | if err != nil {
92 | return nil, nil, err
93 | }
94 |
95 | return resp, redirects, nil
96 | }
97 |
98 | func Request(ctx context.Context, p interfaces.Vendor, reqOpt *interfaces.RequestOptions) (duration uint16, bodyBytes []byte, resp *http.Response, redirects []string) {
99 | var err error
100 |
101 | start := time.Now()
102 | resp, redirects, err = RequestUnsafe(ctx, p, reqOpt)
103 | if err != nil {
104 | return
105 | }
106 | defer resp.Body.Close()
107 |
108 | bodyBytes, err = ioutil.ReadAll(resp.Body)
109 | if err != nil {
110 | return
111 | }
112 |
113 | duration = uint16(time.Since(start) / time.Millisecond)
114 | return
115 | }
116 |
117 | func RequestWithRetry(p interfaces.Vendor, retry int, timeoutMillisecond int64, reqOpt *interfaces.RequestOptions) ([]byte, *http.Response, []string) {
118 | var resp *http.Response = nil
119 | var retBody []byte = nil
120 | var redirects = []string{}
121 |
122 | for i := 0; resp == nil && i < structs.WithIn(retry, 1, 10); i += 1 {
123 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMillisecond)*time.Millisecond)
124 | _, retBody, resp, redirects = Request(ctx, p, reqOpt)
125 | cancel()
126 | }
127 |
128 | return retBody, resp, redirects
129 | }
130 |
131 | func NetCat(ctx context.Context, p interfaces.Vendor, addr string, data []byte, network interfaces.RequestOptionsNetwork) ([]byte, error) {
132 | var conn net.Conn = nil
133 | err := fmt.Errorf("proxy is not ready")
134 | if p != nil {
135 | if p.Status() == interfaces.VStatusOperational {
136 | conn, err = p.DialTCP(ctx, addr, network)
137 | }
138 | } else {
139 | conn, err = net.Dial(network.String(), addr)
140 | }
141 |
142 | if err != nil {
143 | return nil, err
144 | }
145 |
146 | defer conn.Close()
147 | if _, err := conn.Write(data); err != nil {
148 | return nil, err
149 | }
150 |
151 | var buf bytes.Buffer
152 | if _, err := io.Copy(&buf, conn); err != nil {
153 | return nil, err
154 | }
155 |
156 | return buf.Bytes(), nil
157 | }
158 |
159 | func NetCatWithRetry(p interfaces.Vendor, retry int, timeoutMillisecond int64, addr string, data []byte, network interfaces.RequestOptionsNetwork) ([]byte, error) {
160 | var retBody []byte = nil
161 | var err = fmt.Errorf("request not send")
162 |
163 | for i := 0; err != nil && i < structs.WithIn(retry, 1, 10); i += 1 {
164 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMillisecond)*time.Millisecond)
165 | retBody, err = NetCat(ctx, p, addr, data, network)
166 | cancel()
167 | }
168 |
169 | return retBody, err
170 | }
171 |
--------------------------------------------------------------------------------
/vendors/invalid/vendor.go:
--------------------------------------------------------------------------------
1 | package invalid
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/metacubex/mihomo/constant"
7 | "net"
8 |
9 | "github.com/airportr/miaospeed/interfaces"
10 | )
11 |
12 | type Invalid struct {
13 | name string
14 | }
15 |
16 | func (c *Invalid) Proxy() constant.Proxy {
17 | return nil
18 | }
19 |
20 | func (c *Invalid) Type() interfaces.VendorType {
21 | return interfaces.VendorInvalid
22 | }
23 |
24 | func (c *Invalid) Status() interfaces.VendorStatus {
25 | return interfaces.VStatusNotReady
26 | }
27 |
28 | func (c *Invalid) Build(proxyName string, proxyInfo string) interfaces.Vendor {
29 | c.name = proxyName
30 | return c
31 | }
32 |
33 | func (c *Invalid) DialTCP(ctx context.Context, url string, network interfaces.RequestOptionsNetwork) (net.Conn, error) {
34 | return nil, fmt.Errorf("the vendor is invalid")
35 | }
36 |
37 | func (c *Invalid) DialUDP(ctx context.Context, url string) (net.PacketConn, error) {
38 | return nil, fmt.Errorf("the vendor is invalid")
39 | }
40 |
41 | func (c *Invalid) ProxyInfo() interfaces.ProxyInfo {
42 | return interfaces.ProxyInfo{
43 | Name: c.name,
44 | Type: interfaces.ProxyInvalid,
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/vendors/local/metadata.go:
--------------------------------------------------------------------------------
1 | package local
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "strconv"
7 | )
8 |
9 | func urlToMetadata(rawURL string) (string, uint16, error) {
10 | u, err := url.Parse(rawURL)
11 | if err != nil {
12 | return "", 0, fmt.Errorf("cannot parse the url")
13 | }
14 |
15 | port := u.Port()
16 | if port == "" {
17 | switch u.Scheme {
18 | case "https":
19 | port = "443"
20 | case "http":
21 | port = "80"
22 | default:
23 | return "", 0, fmt.Errorf("unknown url scheme")
24 | }
25 | }
26 |
27 | portUint8, err := strconv.ParseUint(port, 10, 16)
28 | if err != nil {
29 | return "", 0, fmt.Errorf("cannot parse the port number")
30 | }
31 |
32 | return u.Hostname(), uint16(portUint8), nil
33 | }
34 |
--------------------------------------------------------------------------------
/vendors/local/vendor.go:
--------------------------------------------------------------------------------
1 | package local
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/metacubex/mihomo/constant"
7 | "net"
8 |
9 | "github.com/airportr/miaospeed/interfaces"
10 | )
11 |
12 | type Local struct {
13 | name string
14 | info string
15 | }
16 |
17 | func (c *Local) Proxy() constant.Proxy {
18 | return nil
19 | }
20 |
21 | func (c *Local) Type() interfaces.VendorType {
22 | return interfaces.VendorLocal
23 | }
24 |
25 | func (c *Local) Status() interfaces.VendorStatus {
26 | return interfaces.VStatusOperational
27 | }
28 |
29 | func (c *Local) Build(proxyName string, proxyInfo string) interfaces.Vendor {
30 | c.name = proxyName
31 | c.info = proxyInfo
32 | return c
33 | }
34 |
35 | func (c *Local) DialTCP(_ context.Context, url string, network interfaces.RequestOptionsNetwork) (net.Conn, error) {
36 | if hostname, port, err := urlToMetadata(url); err != nil {
37 | return nil, err
38 | } else {
39 | return net.Dial(network.String(), fmt.Sprintf("%s:%d", hostname, port))
40 | }
41 | }
42 |
43 | func (c *Local) DialUDP(_ context.Context, _ string) (net.PacketConn, error) {
44 | return nil, fmt.Errorf("local test does not support udp yet")
45 |
46 | }
47 | func (c *Local) ProxyInfo() interfaces.ProxyInfo {
48 | return interfaces.ProxyInfo{
49 | Name: c.name,
50 | Address: "127.0.0.1",
51 | Type: interfaces.Parse("Invalid"),
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/vendors/vendors.go:
--------------------------------------------------------------------------------
1 | package vendors
2 |
3 | import (
4 | "github.com/airportr/miaospeed/interfaces"
5 |
6 | "github.com/airportr/miaospeed/vendors/clash"
7 | "github.com/airportr/miaospeed/vendors/invalid"
8 | "github.com/airportr/miaospeed/vendors/local"
9 | )
10 |
11 | var registeredList = map[interfaces.VendorType]func() interfaces.Vendor{
12 | interfaces.VendorLocal: func() interfaces.Vendor {
13 | return &local.Local{}
14 | },
15 | interfaces.VendorClash: func() interfaces.Vendor {
16 | return &clash.Clash{}
17 | },
18 | }
19 |
20 | func Find(vendorType interfaces.VendorType) interfaces.Vendor {
21 | if vendor, ok := registeredList[vendorType]; ok {
22 | return vendor()
23 | }
24 |
25 | return &invalid.Invalid{}
26 | }
27 |
--------------------------------------------------------------------------------