├── .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 | Docker Pulls 10 | Docker Image Size 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 | Docker Pulls 10 | Docker Image Size 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 | --------------------------------------------------------------------------------