├── .gitignore
├── Preview.png
├── sync.go
├── blockList-Optional.json
├── getproxy_other.go
├── .github
└── workflows
│ ├── ci-test.yaml
│ ├── docker.yaml
│ └── release.yaml
├── blockList.json
├── util_test.go
├── ipBlockList.txt
├── doc
└── SyncServer.md
├── main.go
├── LICENSE
├── Dockerfile
├── getproxy_common.go
├── entrypoint.sh
├── go.mod
├── server.go
├── main_windows.go
├── log.go
├── config.toml
├── config.json
├── client.go
├── ip.go
├── SyncServer.go
├── util.go
├── request.go
├── client_BitComet.go
├── client_Transmission.go
├── lang
└── en.json
├── torrent.go
├── i18n.go
├── client_qBittorrent.go
├── peer.go
├── README.md
├── go.sum
├── console.go
├── README.en.md
└── config.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | logs/
3 | qBittorrent-ClientBlocker
4 | qBittorrent-ClientBlocker.exe
5 |
--------------------------------------------------------------------------------
/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simple-Tracker/qBittorrent-ClientBlocker/HEAD/Preview.png
--------------------------------------------------------------------------------
/sync.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | var Mutex = sync.Mutex{}
8 | var WaitGroup = sync.WaitGroup{}
9 |
--------------------------------------------------------------------------------
/blockList-Optional.json:
--------------------------------------------------------------------------------
1 | [
2 | "^-UW\\w{4}-", // uTorrent Web.
3 | "^-SP(([0-2]\\d{3})|(3[0-5]\\d{2}))-",
4 | "StellarPlayer", // 恒星播放器.
5 | "dandanplay" // 弹弹 Play.
6 | ]
7 |
--------------------------------------------------------------------------------
/getproxy_other.go:
--------------------------------------------------------------------------------
1 | //go:build !darwin && !windows && !linux
2 |
3 | package main
4 |
5 | import (
6 | "net/http"
7 | "net/url"
8 | )
9 |
10 | var getproxy_notified = false
11 |
12 | func GetProxy(r *http.Request) (*url.URL, error) {
13 | if r == nil {
14 | if !getproxy_notified {
15 | getproxy_notified = true
16 | Log("GetProxy", GetLangText("GetProxy_UseEnvVar"), true)
17 | }
18 | return nil, nil
19 | }
20 |
21 | return http.ProxyFromEnvironment(r)
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci-test.yaml:
--------------------------------------------------------------------------------
1 | name: 'Dev-CI-Test'
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: ['dev']
7 | pull_request:
8 | branches: ['dev']
9 |
10 | jobs:
11 | Dev-CI-Test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: 'Checkout'
15 | uses: actions/checkout@v4
16 | - name: 'Setup Go'
17 | uses: actions/setup-go@v4
18 | with:
19 | go-version: '1.20.13'
20 | - name: 'Test'
21 | run: 'go test -v ./... -race'
22 |
--------------------------------------------------------------------------------
/blockList.json:
--------------------------------------------------------------------------------
1 | [
2 | "Gopeed dev",
3 | "Rain 0.0.0",
4 | "Taipei-Torrent dev",
5 | "Xunlei",
6 | "^-(SD|XF|QD|BN|DL)",
7 | "^-DT",
8 | "^-GT0002",
9 | "^-HP",
10 | "^-RN",
11 | "^-XL",
12 | "^-XM",
13 | "anacrolix[ /]torrent v?([0-1]\\\\.(([0-9]|[0-4][0-9]|[0-5][0-2])\\\\.[0-9]+|(53\\\\.[0-2]( |$)))|unknown)",
14 | "cacao_torrent",
15 | "dt[ /]torrent",
16 | "go[ \\\\.]torrent",
17 | "gobind",
18 | "hp[ /]torrent",
19 | "ljyun.cn",
20 | "offline-download",
21 | "qBittorrent[ /]3\\\\.3\\\\.15",
22 | "trafficConsume",
23 | "xm[ /]torrent",
24 | "ޭ__"
25 | ]
26 |
--------------------------------------------------------------------------------
/util_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sync"
5 | "testing"
6 | )
7 |
8 | func TestEraseSyncMap(t *testing.T) {
9 | testCases := []struct {
10 | name string
11 | data map[any]any
12 | }{
13 | {
14 | name: "TestEraseSyncMap",
15 | data: map[any]any{"key": "val"},
16 | },
17 | }
18 |
19 | for _, tc := range testCases {
20 | t.Run(tc.name, func(t *testing.T) {
21 | m := &sync.Map{}
22 | for k, v := range tc.data {
23 | m.Store(k, v)
24 | }
25 |
26 | EraseSyncMap(m)
27 | m.Range(func(k, v any) bool {
28 | t.Errorf("EraseSyncMap() = %v, want %v", v, nil)
29 | return false
30 | })
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ipBlockList.txt:
--------------------------------------------------------------------------------
1 | // Credit: https://docs.qq.com/doc/DQnJBTGJjSFZBR2JW.
2 | 112.87.15.0/24
3 | 58.241.88.0/24
4 | 1.180.24.0/23
5 | 36.250.141.0/24
6 | 36.102.218.0/24
7 | 101.69.63.0/24
8 | 112.45.16.0/24
9 | 112.45.20.0/24
10 | 115.231.84.120/29
11 | 115.231.84.128/28
12 | 122.224.33.0/24
13 | 123.184.152.0/24
14 | 218.7.138.0/24
15 | 221.11.96.0/24
16 | 221.203.3.0/24
17 | 221.203.6.0/24
18 | 223.78.79.0/24
19 | 223.78.80.0/24
20 | 2002:df4e:4f00::/48
21 | 2002:df4e:5000::/48
22 | 2408:862e:ff:ff0d::/60
23 | 2408:8631:2e09:d05::/60
24 | 2408:8738:6000:d::/60
25 | 2409:873c:f03:6000::/56
26 | 240e:90c:2000:301::/60
27 | 240e:90e:2000:2006::/60
28 | 240e:918:8008::/48
29 |
--------------------------------------------------------------------------------
/doc/SyncServer.md:
--------------------------------------------------------------------------------
1 | # SyncServer
2 | SyncServer 同步服务器是一个设想, 在设想中: 屏蔽器客户端发送所有的 TorrentMap (其中包括 Peer), 服务器即掌握所有连接至该服务器的屏蔽器客户端的 Torrent 及 Peer 的相关信息. 服务器通过所掌握的信息作出决策, 这些决策随本次或下次请求返回给客户端.
3 |
4 | ## 请求结构
5 | 一个示例应该如下:
6 | ```
7 | POST /api/syncserver
8 | Content-Type: application/json
9 |
10 | {
11 | "version": 1,
12 | "timestamp": 1714306700,
13 | "token": "",
14 | "torrentMap": {
15 | "(InfoHash)": {
16 | "size": 1048576,
17 | "peers": {
18 | "(PeerIP)": {
19 | "net": "(Unknown)",
20 | "port": {
21 | (Port): true
22 | },
23 | "progress": 0.233,
24 | "downloaded": 1048576,
25 | "uploaded": 1024
26 | }
27 | }
28 | }
29 | }
30 | }
31 | ```
32 |
33 | ## 响应结构
34 | 客户端会根据服务器的响应重新调整自身 ```Interval```, 若要避免尖峰, 可在每次返回时使用不同的 Offset 调整 ```Interval```. 若没有发生错误, 则 ```Status``` 应该置空.
35 | 一个示例应该如下:
36 | ```
37 | {
38 | "interval": 300,
39 | "status": "",
40 | "blockIPRule": {
41 | "(Reason)": [
42 | "123.132.213.231/32"
43 | ]
44 | }
45 | }
46 | ```
47 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package main
4 |
5 | func Platform_Stop() {
6 | }
7 | func main() {
8 | /*
9 | torrentMap2 := make(map[string]TorrentInfoStruct)
10 | peers := make(map[string]PeerInfoStruct)
11 | peerPortMap := make(map[int]bool)
12 | peerPortMap[233] = true
13 | peers["testpeer"] = PeerInfoStruct { Port: peerPortMap, Progress: 0.1, Uploaded: 123 }
14 | torrentMap2["testhash"] = TorrentInfoStruct { Size: 233, Peers: peers }
15 | lastTorrentMap2 := make(map[string]TorrentInfoStruct)
16 | DeepCopyTorrentMap(torrentMap2, lastTorrentMap2)
17 |
18 | for torrentInfoHash, torrentInfo := range lastTorrentMap2 {
19 | Log("Test", "%s %d", false, torrentInfoHash, torrentInfo.Size)
20 | for peerIP, peerInfo := range torrentInfo.Peers {
21 | Log("Test", "%s %v", false, peerIP, peerInfo.Progress)
22 | for port, _ := range peerInfo.Port {
23 | Log("Test", "%d", false, port)
24 | }
25 | }
26 | }
27 | return
28 | */
29 |
30 | if PrepareEnv() {
31 | RunConsole()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Simple-Tracker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=${BUILDPLATFORM} golang:1.20.13-alpine AS go
2 | WORKDIR /app
3 |
4 | ARG BUILDOS BUILDARCH TARGETOS TARGETARCH GITHUB_REF PROGRAM_NIGHTLY
5 | ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=7
6 |
7 | RUN PROGRAM_VERSION="$(basename ${GITHUB_REF})"; \
8 | if [ "${GOARCH}" == 'arm' ]; then \
9 | PROGRAM_VERSION="${PROGRAM_VERSION} (${TARGETOS}, ${TARGETARCH}v7)"; \
10 | else \
11 | PROGRAM_VERSION="${PROGRAM_VERSION} (${TARGETOS}, ${TARGETARCH})"; \
12 | fi; \
13 | if [ "${PROGRAM_NIGHTLY}" == 'true' ]; then \
14 | PROGRAM_VERSION="${PROGRAM_VERSION} (Nightly)"; \
15 | fi; \
16 | echo "export PROGRAM_VERSION='${PROGRAM_VERSION}'" > /envfile
17 |
18 | RUN . /envfile; echo "Running on ${BUILDOS}/${BUILDARCH}, Building for ${TARGETOS}/${TARGETARCH}, Version: ${PROGRAM_VERSION}"
19 |
20 | ADD lang/ *LICENSE* *.md *.go *.sh *.txt *.json go.mod go.sum ./
21 |
22 | RUN go mod download
23 | RUN . /envfile; go build -ldflags "-w -X \"main.programVersion=${PROGRAM_VERSION}\"" -o qBittorrent-ClientBlocker
24 | RUN rm -f *.go go.mod go.sum
25 |
26 | FROM alpine
27 | WORKDIR /app
28 |
29 | COPY --from=go /app ./
30 | RUN chmod +x ./entrypoint.sh
31 | RUN apk update && apk add --no-cache jq socat
32 |
33 | ENTRYPOINT ["/app/entrypoint.sh"]
34 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yaml:
--------------------------------------------------------------------------------
1 | name: 'Release-Docker-Image'
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | release:
7 | types: ['created']
8 |
9 | jobs:
10 | Release-Docker-Image:
11 | runs-on: 'ubuntu-latest'
12 | steps:
13 | - name: 'Checkout'
14 | uses: 'actions/checkout@v4'
15 | - name: 'Setup Docker Buildx'
16 | uses: docker/setup-buildx-action@v3
17 | - name: 'Login to Docker Hub'
18 | uses: 'docker/login-action@v3'
19 | if: ${{ github.event_name == 'release' }}
20 | with:
21 | username: ${{ secrets.DOCKERHUB_USERNAME }}
22 | password: ${{ secrets.DOCKERHUB_TOKEN }}
23 | - name: 'Extract metadata (tags, labels) for Docker'
24 | id: 'meta'
25 | uses: 'docker/metadata-action@v5'
26 | with:
27 | images: 'simpletracker/qbittorrent-clientblocker'
28 | - name: ${{ github.event_name == 'release' && 'Build and Push Docker image' || 'Build Docker image' }}
29 | uses: 'docker/build-push-action@v5'
30 | with:
31 | context: '.'
32 | file: './Dockerfile'
33 | push: ${{ github.event_name == 'release' && true || false }}
34 | platforms: 'linux/386,linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le'
35 | tags: ${{ steps.meta.outputs.tags }}
36 | labels: ${{ steps.meta.outputs.labels }}
37 | build-args: |
38 | "GITHUB_REF=${{ github.ref }}"
39 | "PROGRAM_NIGHTLY=${{ github.event_name != 'release' && 'true' || 'false' }}"
40 |
--------------------------------------------------------------------------------
/getproxy_common.go:
--------------------------------------------------------------------------------
1 | //go:build darwin || windows || linux
2 |
3 | package main
4 |
5 | import (
6 | "github.com/bdwyertech/go-get-proxied/proxy"
7 | "net/http"
8 | "net/url"
9 | )
10 |
11 | var getproxy_httpProxyURL *url.URL
12 | var getproxy_httpsProxyURL *url.URL
13 |
14 | func GetProxy(r *http.Request) (*url.URL, error) {
15 | if r == nil && getproxy_httpProxyURL == nil && getproxy_httpsProxyURL == nil {
16 | proxyProvider := proxy.NewProvider("")
17 |
18 | httpProxy := proxyProvider.GetHTTPProxy("")
19 | if httpProxy != nil {
20 | getproxy_httpProxyURL = httpProxy.URL()
21 | if getproxy_httpProxyURL.Scheme == "" {
22 | getproxy_httpProxyURL.Scheme = "http"
23 | }
24 |
25 | Log("GetProxy", GetLangText("GetProxy_ProxyFound"), true, "HTTP", getproxy_httpProxyURL.String(), httpProxy.Src())
26 | }
27 |
28 | httpsProxy := proxyProvider.GetHTTPSProxy("")
29 | if httpsProxy != nil {
30 | getproxy_httpsProxyURL = httpsProxy.URL()
31 | if getproxy_httpsProxyURL.Scheme == "" {
32 | getproxy_httpsProxyURL.Scheme = "http"
33 | }
34 |
35 | Log("GetProxy", GetLangText("GetProxy_ProxyFound"), true, "HTTPS", getproxy_httpsProxyURL.String(), httpsProxy.Src())
36 | }
37 |
38 | if getproxy_httpProxyURL == nil || getproxy_httpsProxyURL == nil {
39 | Log("GetProxy", GetLangText("GetProxy_ProxyNotFound"), true, "HTTP/HTTPS")
40 | }
41 | } else if r != nil {
42 | if r.URL.Scheme == "https" {
43 | return getproxy_httpsProxyURL, nil
44 | } else if r.URL.Scheme == "http" {
45 | return getproxy_httpProxyURL, nil
46 | }
47 | }
48 |
49 | return nil, nil
50 | }
51 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ -n "$useENV" ]; then
4 | echo "Generate config from env."
5 |
6 | # Convert $blockList to json array
7 | tmpBlockList='[]'
8 | tmpIPBlockList='[]'
9 | if [ -n "$blockList" ]; then
10 | tmpBlockList=$(echo $blockList | jq '.')
11 | fi
12 | if [ -n "$ipBlockList" ]; then
13 | tmpIPBlockList=$(echo $ipBlockList | jq '.')
14 | fi
15 |
16 | envKVPair=$(jq -n 'env|to_entries[]')
17 |
18 | # Keep username and password string
19 | # Keep blockList json array
20 | # Convert "true" to true, "false" to false, digital string to number
21 | configKVPair=$(echo $envKVPair | jq --argjson tmpBlockList "$tmpBlockList" --argjson tmpIPBlockList "$tmpIPBlockList" '{
22 | (.key): (
23 | if (.key|ascii_downcase) == "clientusername" or (.key|ascii_downcase) == "clientpassword" then .value
24 | elif (.key|ascii_downcase) == "blocklist" then $tmpBlockList
25 | elif (.key|ascii_downcase) == "ipblocklist" then $tmpIPBlockList
26 | else .value|(
27 | if . == "true" then true
28 | elif . == "false" then false
29 | else (tonumber? // .)
30 | end)
31 | end
32 | )
33 | }')
34 |
35 | (echo $configKVPair | jq -s add) > config_additional.json
36 | fi
37 |
38 | commandArgStr=''
39 | if [ -n "$configPath" ]; then
40 | commandArgStr="-c $configPath"
41 | fi
42 | if [ -n "$additionalConfigPath" ]; then
43 | commandArgStr="$commandArgStr -ca $additionalConfigPath"
44 | fi
45 | exec ./qBittorrent-ClientBlocker $commandArgStr
46 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Simple-Tracker/qBittorrent-ClientBlocker
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/PuerkitoBio/goquery v1.9.1
7 | github.com/Xuanwo/go-locale v1.1.0
8 | github.com/bdwyertech/go-get-proxied v0.0.0-20221029171534-ea033ac5f9fa
9 | github.com/dlclark/regexp2 v1.11.0
10 | github.com/getlantern/systray v1.2.2
11 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e
12 | github.com/pelletier/go-toml/v2 v2.2.2
13 | github.com/tidwall/jsonc v0.3.2
14 | golang.design/x/hotkey v0.4.1
15 | )
16 |
17 | require (
18 | github.com/andybalholm/cascadia v1.3.2 // indirect
19 | github.com/bdwyertech/go-scutil v0.0.0-20210306002117-b25267f54e45 // indirect
20 | github.com/darren/gpac v0.0.0-20210609082804-b56d6523a3af // indirect
21 | github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d // indirect
22 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
23 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
24 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
25 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
26 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
27 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
28 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
29 | github.com/go-stack/stack v1.8.0 // indirect
30 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
31 | golang.org/x/net v0.21.0 // indirect
32 | golang.org/x/sys v0.17.0 // indirect
33 | golang.org/x/text v0.16.0 // indirect
34 | )
35 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net"
6 | "net/http"
7 | "strings"
8 | )
9 |
10 | var Server_Status bool = false
11 | var Server_httpListen net.Listener
12 |
13 | type httpServerHandler struct {
14 | }
15 |
16 | func (h *httpServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
17 | if r.Method != "GET" {
18 | w.WriteHeader(405)
19 | w.Write([]byte("405: Method Not Allowed."))
20 | return
21 | }
22 |
23 | if currentClientType == "Transmission" && Tr_ProcessHTTP(w, r) {
24 | return
25 | }
26 |
27 | w.WriteHeader(404)
28 | w.Write([]byte("404: Not Found."))
29 | }
30 | func StartServer() {
31 | if Server_Status {
32 | return
33 | }
34 |
35 | listenType := "tcp4"
36 | if IsIPv6(config.Listen) {
37 | listenType = "tcp6"
38 | }
39 |
40 | httpListen, err := net.Listen(listenType, strings.SplitN(config.Listen, "/", 2)[0])
41 | if err != nil {
42 | Log("StartServer", GetLangText("Error-StartServer_Listen"), true, err.Error())
43 | return
44 | }
45 |
46 | Server_Status = true
47 | Server_httpListen = httpListen
48 |
49 | httpServer.SetKeepAlivesEnabled(false)
50 | if err := httpServer.Serve(Server_httpListen); err != http.ErrServerClosed {
51 | Log("StartServer", GetLangText("Error-StartServer_Serve"), true, err.Error())
52 | Server_Status = false
53 | }
54 | }
55 | func StopServer() {
56 | if !Server_Status {
57 | return
58 | }
59 |
60 | if err := httpServer.Shutdown(context.Background()); err != nil {
61 | Log("StopServer", GetLangText("Error-StopServer"), true, err.Error())
62 | }
63 |
64 | // 出于保险起见, 再次关闭似乎已被 httpServer 同时关闭的 Listener, 但无视错误.
65 | Server_httpListen.Close()
66 | }
67 |
--------------------------------------------------------------------------------
/main_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package main
4 |
5 | import (
6 | "github.com/getlantern/systray"
7 | "github.com/lxn/win"
8 | "golang.design/x/hotkey"
9 | )
10 |
11 | var showWindow = true
12 | var programHotkey = hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModAlt}, hotkey.KeyB)
13 |
14 | func Platform_ShowOrHiddenWindow() {
15 | consoleWindow := win.GetConsoleWindow()
16 | if showWindow {
17 | Log("Debug-ShowOrHiddenWindow", GetLangText("Debug-ShowOrHiddenWindow_HideWindow"), false)
18 | showWindow = false
19 | win.ShowWindow(consoleWindow, win.SW_HIDE)
20 | } else {
21 | Log("Debug-ShowOrHiddenWindow", GetLangText("Debug-ShowOrHiddenWindow_ShowWindow"), false)
22 | showWindow = true
23 | win.ShowWindow(consoleWindow, win.SW_SHOW)
24 | }
25 | }
26 | func Platform_Stop() {
27 | programHotkey.Unregister()
28 | systray.Quit()
29 | }
30 | func RegHotKey() {
31 | if !needRegHotKey {
32 | return
33 | }
34 |
35 | err := programHotkey.Register()
36 | if err != nil {
37 | Log("RegHotKey", GetLangText("Error-RegHotkey"), false, err.Error())
38 | return
39 | }
40 | Log("RegHotKey", GetLangText("Success-RegHotkey"), false)
41 |
42 | for range programHotkey.Keydown() {
43 | Platform_ShowOrHiddenWindow()
44 | }
45 | }
46 | func RegSysTray() {
47 | if needHideSystray {
48 | return
49 | }
50 |
51 | systray.Run(func() {
52 | systray.SetIcon(icon_Windows)
53 | systray.SetTitle(programName)
54 | mShow := systray.AddMenuItem("显示/隐藏", "显示/隐藏程序")
55 | mQuit := systray.AddMenuItem("退出", "退出程序")
56 |
57 | go func() {
58 | for {
59 | select {
60 | case <-mShow.ClickedCh:
61 | Platform_ShowOrHiddenWindow()
62 | case <-mQuit.ClickedCh:
63 | systray.Quit()
64 | }
65 | }
66 | }()
67 | }, func() {
68 | ReqStop()
69 | })
70 | }
71 | func main() {
72 | if PrepareEnv() {
73 | if needHideWindow && showWindow {
74 | Platform_ShowOrHiddenWindow()
75 | }
76 | go RegHotKey()
77 | go RegSysTray()
78 | RunConsole()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/log.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "strings"
8 | )
9 |
10 | var todayStr = ""
11 | var lastLogPath = ""
12 | var logFile *os.File
13 | var logwriter = LogWriter{}
14 |
15 | type LogWriter struct {
16 | w io.Writer
17 | }
18 |
19 | func (w LogWriter) Write(p []byte) (n int, err error) {
20 | Log("LogWriter", string(p), true)
21 | return len(p), nil
22 | }
23 | func Log(module string, str string, logToFile bool, args ...interface{}) {
24 | if !strings.HasPrefix(module, "Debug") {
25 | if module == "LogWriter" {
26 | str = StrTrim(str)
27 | if strings.HasPrefix(str, "[proxy.Provider") {
28 | return
29 | }
30 | }
31 | } else if config.Debug {
32 | if config.LogDebug {
33 | logToFile = true
34 | }
35 | } else {
36 | return
37 | }
38 |
39 | logStr := fmt.Sprintf("["+GetDateTime(true)+"]["+module+"] "+str+".\n", args...)
40 | if config.LogToFile && logToFile && logFile != nil {
41 | if _, err := logFile.Write([]byte(logStr)); err != nil {
42 | Log("Log", GetLangText("Error-Log_Write"), false, err.Error())
43 | }
44 | }
45 |
46 | fmt.Print(logStr)
47 | }
48 | func LoadLog() bool {
49 | if !config.LogToFile || config.LogPath == "" {
50 | return false
51 | }
52 |
53 | if err := os.Mkdir(config.LogPath, os.ModePerm); err != nil && !os.IsExist(err) {
54 | Log("LoadLog", GetLangText("Error-LoadLog_Mkdir"), false, err.Error())
55 | return false
56 | }
57 |
58 | tmpTodayStr := GetDateTime(false)
59 | newDay := (todayStr != tmpTodayStr)
60 | newLogPath := (lastLogPath != config.LogPath)
61 |
62 | if !newDay && !newLogPath {
63 | return true
64 | }
65 |
66 | if newDay {
67 | todayStr = tmpTodayStr
68 | }
69 |
70 | if newLogPath {
71 | if lastLogPath != "" {
72 | Log("LoadLog", GetLangText("LoadLog_HotReload"), false, config.LogPath)
73 | }
74 | lastLogPath = config.LogPath
75 | }
76 |
77 | tLogFile, err := os.OpenFile(config.LogPath+"/"+todayStr+".txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
78 | if err != nil {
79 | tLogFile.Close()
80 | tLogFile = nil
81 | Log("LoadLog", GetLangText("Error-LoadLog_Close"), false, err.Error())
82 | return false
83 | }
84 |
85 | logFile.Close()
86 | logFile = tLogFile
87 |
88 | return true
89 | }
90 |
--------------------------------------------------------------------------------
/config.toml:
--------------------------------------------------------------------------------
1 | checkUpdate = true
2 | debug = false
3 | interval = 6
4 | cleanInterval = 3_600
5 | banTime = 86_400
6 | timeout = 6
7 | logToFile = true
8 | clientType = ""
9 | clientURL = ""
10 | clientUsername = ""
11 | clientPassword = ""
12 | useBasicAuth = false
13 | useShadowBan = true
14 | skipCertVerification = false
15 | blockList = []
16 | blockListFile = [
17 | "blockList.json",
18 | # "blockList-Optional.json",
19 | ]
20 | blockListURL = [
21 | "https://bta.iaalai.cn/qBittorrent-ClientBlocker/blockList.json",
22 | # "https://cdn.jsdelivr.net/gh/Simple-Tracker/qBittorrent-ClientBlocker@dev/blockList.json",
23 | ]
24 | portBlockList = []
25 | ipBlockList = []
26 | ipBlockListFile = ["ipBlockList.txt"]
27 | ipBlockListURL = [
28 | "https://bta.iaalai.cn/BTN-Collected-Rules/combine/all.txt",
29 | "https://cdn.jsdelivr.net/gh/PBH-BTN/BTN-Collected-Rules@main/combine/all.txt",
30 | # "https://bta.iaalai.cn/BTN-Collected-Rules/qBittorrent-ClientBlocker/ipBlockList.txt",
31 | "https://cdn.jsdelivr.net/gh/Simple-Tracker/qBittorrent-ClientBlocker@dev/ipBlockList.txt",
32 | ]
33 | # debug_CheckTorrent = false
34 | # debug_CheckPeer = false
35 | # updateInterval = 86_400
36 | # restartInterval = 6
37 | # torrentMapCleanInterval = 60
38 | # banAllPort = true
39 | # banIPCIDR = "/32"
40 | # banIP6CIDR = "/128"
41 | # ignoreEmptyPeer = true
42 | # ignoreNoLeechersTorrent = true
43 | # ignorePTTorrent = true
44 | # ignoreAuthFailed = false
45 | # sleepTime = 20
46 | # proxy = "Auto"
47 | # longConnection = true
48 | # logPath = "logs"
49 | # logDebug = false
50 | # listen = "127.0.0.1:26262"
51 | # fetchFailedThreshold = 0
52 | # execCommand_FetchFailed = ""
53 | # execCommand_Run = ""
54 | # execCommand_Ban = ""
55 | # execCommand_Unban = ""
56 | # syncServerURL = ""
57 | # syncServerToken = ""
58 | # genIPDat = 0
59 | # ipUploadedCheck = false
60 | # ipUpCheckInterval = 300
61 | # ipUpCheckIncrementMB = 38_000
62 | # ipUpCheckPerTorrentRatio = 3
63 | # maxIPPortCount = 0
64 | # banByProgressUploaded = false
65 | # banByPUStartMB = 20
66 | # banByPUStartPrecent = 2
67 | # banByPUAntiErrorRatio = 3
68 | # banByRelativeProgressUploaded = false
69 | # banByRelativePUStartMB = 20
70 | # banByRelativePUStartPrecent = 2
71 | # banByRelativePUAntiErrorRatio = 3
72 | # ignoreByDownloaded = 100
73 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "checkUpdate": true,
3 | "debug": false,
4 | "interval": 6,
5 | "cleanInterval": 3600,
6 | "banTime": 86400,
7 | "timeout": 6,
8 | "logToFile": true,
9 | "clientType": "",
10 | "clientURL": "",
11 | "clientUsername": "",
12 | "clientPassword": "",
13 | "useBasicAuth": false,
14 | "useShadowBan": true,
15 | "skipCertVerification": false,
16 | "blockList": [],
17 | "blockListFile": [
18 | "blockList.json",
19 | //"blockList-Optional.json"
20 | ],
21 | "blockListURL": [
22 | "https://bta.iaalai.cn/qBittorrent-ClientBlocker/blockList.json",
23 | "https://cdn.jsdelivr.net/gh/Simple-Tracker/qBittorrent-ClientBlocker@dev/blockList.json"
24 | ],
25 | "portBlockList": [],
26 | "ipBlockList": [],
27 | "ipBlockListFile": [
28 | "ipBlockList.txt"
29 | ],
30 | "ipBlockListURL": [
31 | "https://bta.iaalai.cn/BTN-Collected-Rules/combine/all.txt",
32 | "https://cdn.jsdelivr.net/gh/PBH-BTN/BTN-Collected-Rules@main/combine/all.txt",
33 | //"https://bta.iaalai.cn/BTN-Collected-Rules/qBittorrent-ClientBlocker/ipBlockList.txt",
34 | "https://cdn.jsdelivr.net/gh/Simple-Tracker/qBittorrent-ClientBlocker@dev/ipBlockList.txt"
35 | ]
36 | /*
37 | "debug_CheckTorrent": false,
38 | "debug_CheckPeer": false,
39 | "updateInterval": 86400,
40 | "restartInterval": 6,
41 | "torrentMapCleanInterval": 60,
42 | "banAllPort": true,
43 | "banIPCIDR": "/32",
44 | "banIP6CIDR": "/128",
45 | "ignoreEmptyPeer": true,
46 | "ignoreNoLeechersTorrent": true,
47 | "ignorePTTorrent": true,
48 | "ignoreAuthFailed": false,
49 | "sleepTime": 20,
50 | "proxy": "Auto",
51 | "longConnection": true,
52 | "logPath": "logs",
53 | "logDebug": false,
54 | "listen": "127.0.0.1:26262",
55 | "fetchFailedThreshold": 0,
56 | "execCommand_FetchFailed": "",
57 | "execCommand_Run": "",
58 | "execCommand_Ban": "",
59 | "execCommand_Unban": "",
60 | "syncServerURL": "",
61 | "syncServerToken": "",
62 | "genIPDat": 0,
63 | "ipUploadedCheck": false,
64 | "ipUpCheckInterval": 300,
65 | "ipUpCheckIncrementMB": 38000,
66 | "ipUpCheckPerTorrentRatio": 3.0,
67 | "maxIPPortCount": 0,
68 | "banByProgressUploaded": false,
69 | "banByPUStartMB": 20,
70 | "banByPUStartPrecent": 2.0,
71 | "banByPUAntiErrorRatio": 3.0,
72 | "banByRelativeProgressUploaded": false,
73 | "banByRelativePUStartMB": 20,
74 | "banByRelativePUStartPrecent": 2.0,
75 | "banByRelativePUAntiErrorRatio": 3.0,
76 | "ignoreByDownloaded": 100,
77 | */
78 | }
79 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | var currentClientType = ""
4 |
5 | // 重复判断 nil 是因为输出的类型转换 (qB_MainDataStruct -> interface{}) 会导致 nil 比较失效.
6 | func IsBanPort() bool {
7 | if currentClientType == "qBittorrent" && qB_useNewBanPeersMethod {
8 | return true
9 | }
10 |
11 | return false
12 | }
13 |
14 | func IsSupportClient() bool {
15 | switch currentClientType {
16 | case "qBittorrent", "Transmission", "BitComet":
17 | return true
18 | }
19 |
20 | return false
21 | }
22 | func InitClient() {
23 | if currentClientType == "Transmission" {
24 | Tr_InitClient()
25 | }
26 | }
27 | func SetURLFromClient() {
28 | // 未设置的情况下, 应按内部客户端顺序逐个测试.
29 | if config.ClientURL == "" {
30 | if !qB_SetURL() {
31 | Tr_SetURL()
32 | }
33 | }
34 | }
35 | func DetectClient() bool {
36 | currentClientType = "qBittorrent"
37 | if config.ClientType == "" || config.ClientType == currentClientType {
38 | if qB_GetAPIVersion() {
39 | Log("DetectClient", GetLangText("Success-DetectClient"), true, currentClientType)
40 | return true
41 | }
42 | }
43 |
44 | currentClientType = "Transmission"
45 | if config.ClientType == "" || config.ClientType == currentClientType {
46 | if Tr_DetectVersion() {
47 | Log("DetectClient", GetLangText("Success-DetectClient"), true, currentClientType)
48 | return true
49 | }
50 | }
51 |
52 | currentClientType = "BitComet"
53 | if config.ClientType == "" || config.ClientType == currentClientType {
54 | if BC_DetectClient() {
55 | Log("DetectClient", GetLangText("Success-DetectClient"), true, currentClientType)
56 | return true
57 | }
58 | }
59 |
60 | if config.ClientType != "" {
61 | currentClientType = config.ClientType
62 | return true
63 | }
64 |
65 | currentClientType = ""
66 | return false
67 | }
68 | func Login() bool {
69 | switch currentClientType {
70 | case "qBittorrent":
71 | return qB_Login()
72 | case "Transmission":
73 | return Tr_Login()
74 | case "BitComet":
75 | return BC_Login()
76 | }
77 |
78 | return false
79 | }
80 | func FetchTorrents() interface{} {
81 | switch currentClientType {
82 | case "qBittorrent":
83 | maindata := qB_FetchTorrents()
84 | if maindata == nil {
85 | return nil
86 | }
87 | return maindata
88 | case "Transmission":
89 | maindata := Tr_FetchTorrents()
90 | if maindata == nil {
91 | return nil
92 | }
93 | return maindata
94 | case "BitComet":
95 | maindata := BC_FetchTorrents()
96 | if maindata == nil {
97 | return nil
98 | }
99 | return maindata
100 | }
101 |
102 | return nil
103 | }
104 | func FetchTorrentPeers(infoHash string) interface{} {
105 | switch currentClientType {
106 | case "qBittorrent":
107 | torrentPeers := qB_FetchTorrentPeers(infoHash)
108 | if torrentPeers == nil {
109 | return nil
110 | }
111 | return torrentPeers
112 | case "BitComet":
113 | torrentPeers := BC_FetchTorrentPeers(infoHash)
114 | if torrentPeers == nil {
115 | return nil
116 | }
117 | return torrentPeers
118 | }
119 |
120 | return nil
121 | }
122 | func SubmitBlockPeer(blockPeerMap map[string]BlockPeerInfoStruct) bool {
123 | if blockPeerMap == nil {
124 | return true
125 | }
126 |
127 | switch currentClientType {
128 | case "qBittorrent":
129 | if config.UseShadowBan {
130 | return qB_SubmitShadowBanPeer(blockPeerMap)
131 | } else {
132 | return qB_SubmitBlockPeer(blockPeerMap)
133 | }
134 | case "Transmission":
135 | return Tr_SubmitBlockPeer(blockPeerMap)
136 | }
137 |
138 | return false
139 | }
140 | func TestShadowBanAPI() int {
141 | // -1: Unsupported (Error), 0: Unsupported (Silent), 1: Supported.
142 | switch currentClientType {
143 | case "qBittorrent":
144 | if qB_TestShadowBanAPI() {
145 | return 1
146 | }
147 | return -1
148 | }
149 |
150 | return 0
151 | }
152 |
--------------------------------------------------------------------------------
/ip.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "net"
4 |
5 | type IPInfoStruct struct {
6 | Net *net.IPNet
7 | Port map[int]bool
8 | TorrentUploaded map[string]int64
9 | }
10 |
11 | var ipMap = make(map[string]IPInfoStruct)
12 | var lastIPMap = make(map[string]IPInfoStruct)
13 | var lastIPCleanTimestamp int64 = 0
14 |
15 | func AddIPInfo(cidr *net.IPNet, peerIP string, peerPort int, torrentInfoHash string, peerUploaded int64) {
16 | if !(config.MaxIPPortCount > 0 || (config.IPUploadedCheck && config.IPUpCheckIncrementMB > 0)) {
17 | return
18 | }
19 |
20 | var clientPortMap map[int]bool
21 | var clientTorrentUploadedMap map[string]int64
22 |
23 | if info, exist := ipMap[peerIP]; !exist {
24 | clientPortMap = make(map[int]bool)
25 | clientTorrentUploadedMap = make(map[string]int64)
26 | } else {
27 | clientPortMap = info.Port
28 | clientTorrentUploadedMap = info.TorrentUploaded
29 | }
30 | clientPortMap[peerPort] = true
31 |
32 | if oldPeerUploaded, exist := clientTorrentUploadedMap[torrentInfoHash]; !exist || oldPeerUploaded <= peerUploaded {
33 | clientTorrentUploadedMap[torrentInfoHash] = peerUploaded
34 | } else {
35 | clientTorrentUploadedMap[torrentInfoHash] += peerUploaded
36 | }
37 |
38 | ipMap[peerIP] = IPInfoStruct{Net: cidr, Port: clientPortMap, TorrentUploaded: clientTorrentUploadedMap}
39 | }
40 | func IsIPTooHighUploaded(ipInfo IPInfoStruct, lastIPInfo IPInfoStruct) int64 {
41 | var totalUploaded int64 = 0
42 |
43 | for torrentInfoHash, torrentUploaded := range ipInfo.TorrentUploaded {
44 | if config.IPUpCheckIncrementMB > 0 {
45 | if lastTorrentUploaded, exist := lastIPInfo.TorrentUploaded[torrentInfoHash]; !exist {
46 | totalUploaded += torrentUploaded
47 | } else {
48 | totalUploaded += (torrentUploaded - lastTorrentUploaded)
49 | }
50 | }
51 | }
52 |
53 | if config.IPUpCheckIncrementMB > 0 {
54 | var totalUploadedMB int64 = (totalUploaded / 1024 / 1024)
55 | if totalUploadedMB > int64(config.IPUpCheckIncrementMB) {
56 | return totalUploadedMB
57 | }
58 | }
59 |
60 | return 0
61 | }
62 | func IsMatchCIDR(peerNet *net.IPNet) bool {
63 | if peerNet != nil {
64 | if _, exist := blockCIDRMap[peerNet.String()]; exist {
65 | return true
66 | }
67 | }
68 |
69 | return false
70 | }
71 | func CheckAllIP(ipMap map[string]IPInfoStruct, lastIPMap map[string]IPInfoStruct) int {
72 | if (config.MaxIPPortCount > 0 || (config.IPUploadedCheck && config.IPUpCheckIncrementMB > 0)) && len(lastIPMap) > 0 && currentTimestamp > (lastIPCleanTimestamp+int64(config.IPUpCheckInterval)) {
73 | ipBlockCount := 0
74 |
75 | ipMapLoop:
76 | for ip, ipInfo := range ipMap {
77 | if IsBlockedPeer(ip, -1, true) || len(ipInfo.Port) <= 0 {
78 | continue
79 | }
80 |
81 | for port := range ipInfo.Port {
82 | if IsBlockedPeer(ip, port, true) {
83 | continue ipMapLoop
84 | }
85 | }
86 |
87 | if config.MaxIPPortCount > 0 {
88 | if len(ipInfo.Port) > int(config.MaxIPPortCount) {
89 | Log("CheckAllIP_AddBlockPeer (Too many ports)", "%s:%d", true, ip, -1)
90 | ipBlockCount++
91 | AddBlockPeer("CheckAllIP", "Too many ports", ip, -1, "")
92 | AddBlockCIDR(ip, ipInfo.Net)
93 | continue
94 | }
95 | }
96 |
97 | if lastIPInfo, exist := lastIPMap[ip]; exist {
98 | if uploadDuring := IsIPTooHighUploaded(ipInfo, lastIPInfo); uploadDuring > 0 {
99 | Log("CheckAllIP_AddBlockPeer (Global-Too high uploaded)", "%s:%d (UploadDuring: %.2f MB)", true, ip, -1, uploadDuring)
100 | ipBlockCount++
101 | AddBlockPeer("CheckAllIP", "Global-Too high uploaded", ip, -1, "")
102 | AddBlockCIDR(ip, ipInfo.Net)
103 | }
104 | }
105 | }
106 |
107 | lastIPCleanTimestamp = currentTimestamp
108 | DeepCopyIPMap(ipMap, lastIPMap)
109 |
110 | return ipBlockCount
111 | }
112 |
113 | return 0
114 | }
115 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: 'Release-Go-Binary'
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | release:
7 | types: ['created']
8 |
9 | jobs:
10 | Release-Go-Binary:
11 | name: ${{ matrix.goarch == 'arm' && format('{0}, {1}v{2}', matrix.goos, matrix.goarch, matrix.goarm) || format('{0}, {1}', matrix.goos, matrix.goarch) }}
12 | runs-on: 'ubuntu-latest'
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | goos: [darwin, windows, linux, netbsd, openbsd, freebsd]
17 | goarch: ['386', 'amd64', 'arm', 'arm64']
18 | goarm: ['6']
19 | include:
20 | - goos: windows
21 | goarch: 'arm'
22 | goarm: '7'
23 | - goos: linux
24 | goarch: 'arm'
25 | goarm: '5'
26 | - goos: linux
27 | goarch: '386'
28 | pre_command: 'export CGO_ENABLED=0'
29 | - goos: linux
30 | goarch: 'amd64'
31 | pre_command: 'export CGO_ENABLED=0'
32 | - goos: linux
33 | goarch: 'arm'
34 | goarm: '7'
35 | - goos: linux
36 | goarch: 'mips'
37 | pre_command: 'export GOMIPS=softfloat'
38 | - goos: linux
39 | goarch: 'mipsle'
40 | pre_command: 'export GOMIPS=softfloat'
41 | - goos: linux
42 | goarch: 'mips64'
43 | pre_command: 'export GOMIPS64=softfloat'
44 | - goos: linux
45 | goarch: 'mips64le'
46 | pre_command: 'export GOMIPS64=softfloat'
47 | - goos: linux
48 | goarch: 'riscv64'
49 | - goos: linux
50 | goarch: 'ppc64'
51 | - goos: linux
52 | goarch: 'ppc64le'
53 | - goos: solaris
54 | goarch: 'amd64'
55 | exclude:
56 | - goos: darwin
57 | goarch: '386'
58 | - goos: darwin
59 | goarch: 'arm'
60 | - goos: windows
61 | goarch: 'arm'
62 | goarm: '6'
63 | steps:
64 | - name: 'Checkout'
65 | uses: 'actions/checkout@v4'
66 | - name: 'Set build info'
67 | id: build_info
68 | run: |
69 | echo "tag_version=$(basename ${GITHUB_REF}) (${{ matrix.goarch == 'arm' && format('{0}, {1}v{2}', matrix.goos, matrix.goarch, matrix.goarm) || format('{0}, {1}', matrix.goos, matrix.goarch) }})" >> "${GITHUB_OUTPUT}"
70 | echo "nightly_build_name=$(basename ${GITHUB_REF})_`echo ${GITHUB_SHA} | cut -c1-7`" >> "${GITHUB_OUTPUT}"
71 | echo "nightly_build_version=$(basename ${GITHUB_REF})_`echo ${GITHUB_SHA} | cut -c1-7` (${{ matrix.goarch == 'arm' && format('{0}, {1}v{2}', matrix.goos, matrix.goarch, matrix.goarm) || format('{0}, {1}', matrix.goos, matrix.goarch) }}) (Nightly)" >> "${GITHUB_OUTPUT}"
72 | - name: ${{ github.event_name == 'release' && 'Build' || 'Build (Nightly)' }}
73 | id: build
74 | uses: 'wangyoucao577/go-release-action@v1.50'
75 | with:
76 | github_token: ${{ secrets.GITHUB_TOKEN }}
77 | upload: ${{ github.event_name == 'release' && true || false }}
78 | goos: ${{ matrix.goos }}
79 | goarch: ${{ matrix.goarch }}
80 | goarm: ${{ matrix.goarm }}
81 | ldflags: "-w -X \"main.programVersion=${{ github.event_name == 'release' && steps.build_info.outputs.tag_version || steps.build_info.outputs.nightly_build_version }}\""
82 | pre_command: ${{ matrix.pre_command }}
83 | goversion: 1.20.13
84 | md5sum: false
85 | sha256sum: false
86 | binary_name: 'qBittorrent-ClientBlocker'
87 | extra_files: 'lang/ LICENSE *.md *.txt *.json'
88 | - name: 'Upload GitHub Artifact (Nightly)'
89 | uses: actions/upload-artifact@v4
90 | if: ${{ github.event_name != 'release' }}
91 | with:
92 | name: ${{ github.event.repository.name }}-${{ steps.build_info.outputs.nightly_build_name }}-${{ matrix.goarch == 'arm' && format('{0}-{1}v{2}', matrix.goos, matrix.goarch, matrix.goarm) || format('{0}-{1}', matrix.goos, matrix.goarch) }}
93 | path: ${{ steps.build.outputs.release_asset_dir }}
94 |
--------------------------------------------------------------------------------
/SyncServer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/tidwall/jsonc"
6 | "net"
7 | )
8 |
9 | type SyncServer_ConfigStruct struct {
10 | Interval uint32 `json:"interval"`
11 | Status string `json:"status"`
12 | BlockIPRule map[string][]string `json:"blockIPRule"`
13 | }
14 | type SyncServer_SubmitStruct struct {
15 | Version uint32 `json:"version"`
16 | Timestamp int64 `json:"timestamp"`
17 | Token string `json:"token"`
18 | TorrentMap map[string]TorrentInfoStruct `json:"torrentMap"`
19 | }
20 |
21 | var syncServer_isSubmiting bool = false
22 | var syncServer_lastSync int64 = 0
23 | var syncServer_syncConfig = SyncServer_ConfigStruct{
24 | Interval: 60,
25 | Status: "",
26 | BlockIPRule: make(map[string][]string),
27 | }
28 | var ipBlockCIDRMapFromSyncServerCompiled = make(map[string]*net.IPNet)
29 |
30 | func SyncWithServer_PrepareJSON(torrentMap map[string]TorrentInfoStruct) (bool, string) {
31 | syncJSON, err := json.Marshal(SyncServer_SubmitStruct{Version: 1, Timestamp: currentTimestamp, Token: config.SyncServerToken, TorrentMap: torrentMap})
32 | if err != nil {
33 | Log("SyncWithServer_PrepareJSON", GetLangText("Error-GenJSON"), true, err.Error())
34 | return false, ""
35 | }
36 |
37 | return true, string(syncJSON)
38 | }
39 | func SyncWithServer_Submit(syncJSON string) bool {
40 | _, _, syncServerContent := Submit(config.SyncServerURL, syncJSON, false, false, nil)
41 | syncServer_isSubmiting = false
42 | if syncServerContent == nil {
43 | Log("SyncWithServer", GetLangText("Error-FetchResponse2"), true)
44 | return false
45 | }
46 |
47 | // Max 8MB.
48 | if len(syncServerContent) > 8388608 {
49 | Log("SyncWithServer", GetLangText("Error-LargeFile"), true)
50 | return false
51 | }
52 |
53 | if err := json.Unmarshal(jsonc.ToJSON(syncServerContent), &syncServer_syncConfig); err != nil {
54 | Log("SyncWithServer", GetLangText("Error-ParseConfig"), true, err.Error())
55 | return false
56 | }
57 |
58 | if syncServer_syncConfig.Status != "" {
59 | Log("SyncWithServer", GetLangText("Error-SyncWithServer_ServerError"), true, syncServer_syncConfig.Status)
60 | return false
61 | }
62 |
63 | tmpIPBlockCIDRMapFromSyncServerCompiled := make(map[string]*net.IPNet)
64 |
65 | for reason, ipArr := range syncServer_syncConfig.BlockIPRule {
66 | logReason := false
67 |
68 | for ipBlockListLineNum, ipBlockListLine := range ipArr {
69 | ipBlockListLine = ProcessRemark(ipBlockListLine)
70 | if ipBlockListLine == "" {
71 | Log("Debug-SyncWithServer_Compile", GetLangText("Error-Debug-EmptyLine"), false, ipBlockListLineNum)
72 | continue
73 | }
74 |
75 | if cidr, exists := ipBlockCIDRMapFromSyncServerCompiled[ipBlockListLine]; exists {
76 | tmpIPBlockCIDRMapFromSyncServerCompiled[ipBlockListLine] = cidr
77 | continue
78 | }
79 |
80 | Log("Debug-SyncWithServer_Compile", ":%d %s", false, ipBlockListLineNum, ipBlockListLine)
81 | cidr := ParseIPCIDR(ipBlockListLine)
82 | if cidr == nil {
83 | Log("SyncWithServer_Compile", GetLangText("Error-SyncWithServer_Compile"), true, ipBlockListLineNum, ipBlockListLine)
84 | continue
85 | }
86 |
87 | if !logReason {
88 | logReason = true
89 | Log("SyncWithServer", GetLangText("SyncWithServer_Compile-BlockByReason"), true, reason)
90 | }
91 |
92 | tmpIPBlockCIDRMapFromSyncServerCompiled[ipBlockListLine] = cidr
93 | Log("SyncWithServer_BlockCIDR", ":%d %s", false, ipBlockListLineNum, ipBlockListLine)
94 | }
95 | }
96 |
97 | ipBlockCIDRMapFromSyncServerCompiled = tmpIPBlockCIDRMapFromSyncServerCompiled
98 |
99 | Log("Debug-SyncWithServer", GetLangText("Success-SyncWithServer"), true, len(ipBlockCIDRMapFromSyncServerCompiled))
100 | return true
101 | }
102 | func SyncWithServer_FullSubmit(syncJSON string) bool {
103 | syncServer_isSubmiting = true
104 | syncStatus := SyncWithServer_Submit(syncJSON)
105 | syncServer_isSubmiting = false
106 |
107 | return syncStatus
108 | }
109 | func SyncWithServer() bool {
110 | if config.SyncServerURL == "" || (syncServer_lastSync+int64(syncServer_syncConfig.Interval)) > currentTimestamp || syncServer_isSubmiting {
111 | return true
112 | }
113 |
114 | Log("Debug-SyncWithServer", "In progress..", false)
115 |
116 | status, syncJSON := SyncWithServer_PrepareJSON(torrentMap)
117 | if !status {
118 | return false
119 | }
120 |
121 | syncServer_lastSync = currentTimestamp
122 |
123 | go SyncWithServer_FullSubmit(syncJSON)
124 |
125 | return true
126 | }
127 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "net"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | "sync"
10 | "time"
11 | )
12 |
13 | // Source: https://stackoverflow.com/questions/51459083/deep-copying-maps-in-golang.
14 | func DeepCopyIPMap(src map[string]IPInfoStruct, dest map[string]IPInfoStruct) {
15 | if src != nil && dest != nil {
16 | if jsonStr, err := json.Marshal(src); err == nil {
17 | json.Unmarshal(jsonStr, &dest)
18 | }
19 | }
20 | }
21 | func DeepCopyTorrentMap(src map[string]TorrentInfoStruct, dest map[string]TorrentInfoStruct) {
22 | if src != nil && dest != nil {
23 | if jsonStr, err := json.Marshal(src); err == nil {
24 | json.Unmarshal(jsonStr, &dest)
25 | }
26 | }
27 | }
28 | func IsUnix(path string) bool {
29 | return !strings.Contains(path, "\\")
30 | }
31 | func IsIPv6(ip string) bool {
32 | return (strings.Count(ip, ":") >= 2)
33 | }
34 | func ProcessRemark(str string) string {
35 | // Remove all remarks.
36 | return StrTrim(strings.SplitN(strings.SplitN(str, "#", 2)[0], "//", 2)[0])
37 | }
38 | func StrTrim(str string) string {
39 | return strings.Trim(str, " \n\r")
40 | }
41 | func GetDateTime(withTime bool) string {
42 | formatStr := "2006-01-02"
43 | if withTime {
44 | formatStr += " 15:04:05"
45 | }
46 | return time.Now().Format(formatStr)
47 | }
48 | func CheckPrivateIP(ip string) bool {
49 | ipParsed := net.ParseIP(ip)
50 | if ipParsed == nil {
51 | return false
52 | }
53 | return (ipParsed.IsLoopback() || ipParsed.IsPrivate())
54 | }
55 | func ParseIPCIDR(ip string) *net.IPNet {
56 | if !strings.Contains(ip, "/") {
57 | if IsIPv6(ip) {
58 | ip += "/128"
59 | } else {
60 | ip += "/32"
61 | }
62 | }
63 |
64 | _, cidr, err := net.ParseCIDR(ip)
65 | if err != nil {
66 | return nil
67 | }
68 |
69 | return cidr
70 | }
71 | func ParseIPCIDRByConfig(ip string) *net.IPNet {
72 | cidr := ""
73 |
74 | if IsIPv6(ip) {
75 | if config.BanIP6CIDR != "/128" {
76 | cidr = config.BanIP6CIDR
77 | }
78 | } else {
79 | if config.BanIPCIDR != "/32" {
80 | cidr = config.BanIPCIDR
81 | }
82 | }
83 |
84 | if cidr == "" {
85 | return nil
86 | }
87 |
88 | cidrNet := ParseIPCIDR(ip + cidr)
89 |
90 | if cidrNet == nil {
91 | return nil
92 | }
93 |
94 | return cidrNet
95 | }
96 | func ProcessIP(ip string) string {
97 | ip = strings.ToLower(ip)
98 |
99 | if strings.HasPrefix(ip, "::ffff:") {
100 | return ip[7:]
101 | }
102 |
103 | return ip
104 | }
105 | func GenIPFilter(datType uint32, blockPeerMap map[string]BlockPeerInfoStruct) (int, string) {
106 | ipfilterCount := 0
107 | ipfilterStr := ""
108 |
109 | if datType != 1 && datType != 2 {
110 | return ipfilterCount, ipfilterStr
111 | }
112 |
113 | for peerIP := range blockPeerMap {
114 | if !IsIPv6(peerIP) {
115 | ipfilterCount += 2
116 | if datType == 1 {
117 | ipfilterStr += peerIP + "/32\n"
118 | ipfilterStr += "::ffff:" + peerIP + "/128\n"
119 | } else if datType == 2 {
120 | ipfilterStr += peerIP + " - " + peerIP + " , 000\n"
121 | ipfilterStr += "::ffff:" + peerIP + " - ::ffff:" + peerIP + " , 000\n"
122 | }
123 | } else {
124 | ipfilterCount++
125 | if datType == 1 {
126 | ipfilterStr += peerIP + "/128\n"
127 | } else if datType == 2 {
128 | ipfilterStr += peerIP + " - " + peerIP + " , 000\n"
129 | }
130 | }
131 | }
132 |
133 | return ipfilterCount, ipfilterStr
134 | }
135 | func SaveIPFilter(ipfilterStr string) string {
136 | err := os.WriteFile("ipfilter.dat", []byte(ipfilterStr), 0666)
137 | if err != nil {
138 | return err.Error()
139 | }
140 |
141 | return ""
142 | }
143 | func DeleteIPFilter() bool {
144 | err := os.Remove("ipfilter.dat")
145 | if err != nil {
146 | return false
147 | }
148 |
149 | return true
150 | }
151 | func ParseCommand(command string) []string {
152 | var matchQuote rune = -1
153 | escaped := false
154 | commandPart := []string{""}
155 | commandIndex := 0
156 |
157 | for _, char := range command {
158 | if char == '\\' && matchQuote == -1 {
159 | escaped = true
160 | continue
161 | } else if char == ' ' && matchQuote == -1 {
162 | if escaped {
163 | commandPart[commandIndex] += "\\"
164 | escaped = false
165 | }
166 | if commandPart[commandIndex] != "" {
167 | commandIndex++
168 | commandPart = append(commandPart, "")
169 | }
170 | continue
171 | } else if !escaped && char == '\'' || char == '"' {
172 | if char == matchQuote {
173 | matchQuote = -1
174 | continue
175 | } else if matchQuote == -1 {
176 | matchQuote = char
177 | continue
178 | }
179 | }
180 | if escaped {
181 | commandPart[commandIndex] += "\\"
182 | escaped = false
183 | }
184 | commandPart[commandIndex] += string(char)
185 | }
186 |
187 | return commandPart
188 | }
189 | func ExecCommand(command string) (bool, string, string) {
190 | commandSplit := ParseCommand(command)
191 | Log("Debug-ExecCommand", "Raw: %s, Split (|): %s", false, command, strings.Join(commandSplit, "|"))
192 |
193 | var cmd *exec.Cmd
194 | if len(commandSplit) == 1 {
195 | cmd = exec.Command(commandSplit[0])
196 | } else {
197 | cmd = exec.Command(commandSplit[0], commandSplit[1:]...)
198 | }
199 |
200 | out, err := cmd.CombinedOutput()
201 | if err != nil {
202 | return false, string(out), err.Error()
203 | }
204 |
205 | return true, string(out), ""
206 | }
207 |
208 | // EraseSyncMap is a helper function to erase all elements in a sync.Map.
209 | // This function is supposed to be replaced by `Clear` method after Go1.23 and above.
210 | //
211 | // go:build !go1.23
212 | func EraseSyncMap(m *sync.Map) {
213 | m.Range(func(key, value any) bool {
214 | m.Delete(key)
215 | return true
216 | })
217 | }
218 |
--------------------------------------------------------------------------------
/request.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | "strings"
7 | )
8 |
9 | var fetchFailedCount = 0
10 | var urlETagCache = make(map[string]string)
11 | var urlLastModCache = make(map[string]string)
12 |
13 | func NewRequest(isPOST bool, url string, postdata string, clientReq bool, allowCache bool, withHeader *map[string]string) *http.Request {
14 | var request *http.Request
15 | var err error
16 |
17 | if !isPOST {
18 | request, err = http.NewRequest("GET", url, nil)
19 | } else {
20 | request, err = http.NewRequest("POST", url, strings.NewReader(postdata))
21 | }
22 |
23 | if err != nil {
24 | Log("NewRequest", GetLangText("Error-NewRequest"), true, err.Error())
25 | return nil
26 | }
27 |
28 | setUserAgent := false
29 | setContentType := false
30 |
31 | if withHeader != nil {
32 | for k, v := range *withHeader {
33 | switch strings.ToLower(k) {
34 | case "user-agent":
35 | setUserAgent = true
36 | case "content-type":
37 | setContentType = true
38 | }
39 |
40 | request.Header.Set(k, v)
41 | }
42 | }
43 |
44 | if !setUserAgent {
45 | request.Header.Set("User-Agent", programUserAgent)
46 | }
47 |
48 | if !setContentType && isPOST {
49 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
50 | }
51 |
52 | if clientReq {
53 | if currentClientType == "Transmission" && Tr_csrfToken != "" {
54 | request.Header.Set("X-Transmission-Session-Id", Tr_csrfToken)
55 | }
56 |
57 | if config.UseBasicAuth && config.ClientUsername != "" {
58 | request.SetBasicAuth(config.ClientUsername, config.ClientPassword)
59 | }
60 | } else if !isPOST && allowCache {
61 | if etag, exist := urlETagCache[url]; exist {
62 | request.Header.Set("If-None-Match", etag)
63 | }
64 |
65 | if lastMod, exist := urlLastModCache[url]; exist {
66 | request.Header.Set("If-Modified-Since", lastMod)
67 | }
68 | }
69 |
70 | return request
71 | }
72 | func Fetch(url string, tryLogin bool, clientReq bool, allowCache bool, withHeader *map[string]string) (int, http.Header, []byte) {
73 | request := NewRequest(false, url, "", clientReq, allowCache, withHeader)
74 | if request == nil {
75 | return -1, nil, nil
76 | }
77 |
78 | var response *http.Response
79 | var err error
80 |
81 | if clientReq {
82 | response, err = httpClient.Do(request)
83 | } else {
84 | response, err = httpClientExternal.Do(request)
85 | }
86 |
87 | if err != nil {
88 | if config.FetchFailedThreshold > 0 && config.ExecCommand_FetchFailed != "" {
89 | fetchFailedCount++
90 | if fetchFailedCount >= config.FetchFailedThreshold {
91 | fetchFailedCount = 0
92 | status, out, err := ExecCommand(config.ExecCommand_FetchFailed)
93 |
94 | if status {
95 | Log("Fetch", GetLangText("Success-ExecCommand"), true, out)
96 | } else {
97 | Log("Fetch", GetLangText("Failed-ExecCommand"), true, out, err)
98 | }
99 | }
100 | }
101 | Log("Fetch", GetLangText("Error-FetchResponse"), true, err.Error())
102 | return -2, nil, nil
103 | }
104 |
105 | responseBody, err := ioutil.ReadAll(response.Body)
106 | defer response.Body.Close()
107 |
108 | if err != nil {
109 | Log("Fetch", GetLangText("Error-ReadResponse"), true, err.Error())
110 | return -3, nil, nil
111 | }
112 |
113 | if response.StatusCode == 204 {
114 | Log("Debug-Fetch", GetLangText("Debug-Request_NoContent"), false, url)
115 | return 204, response.Header, nil
116 | }
117 |
118 | if response.StatusCode == 401 {
119 | Log("Fetch", GetLangText("Error-NoAuth"), true)
120 | return 401, response.Header, nil
121 | }
122 |
123 | if response.StatusCode == 403 {
124 | if tryLogin {
125 | Login()
126 | }
127 | Log("Fetch", GetLangText("Error-Forbidden"), true)
128 | return 403, response.Header, nil
129 | }
130 |
131 | if response.StatusCode == 409 {
132 | // 尝试获取并设置 CSRF Token.
133 | if currentClientType == "Transmission" {
134 | trCSRFToken := response.Header.Get("X-Transmission-Session-Id")
135 | if trCSRFToken != "" {
136 | Tr_SetCSRFToken(trCSRFToken)
137 | return 409, nil, nil
138 | }
139 | }
140 |
141 | if tryLogin {
142 | Login()
143 | }
144 |
145 | Log("Fetch", GetLangText("Error-Forbidden"), true)
146 | return 409, response.Header, nil
147 | }
148 |
149 | if response.StatusCode == 404 {
150 | Log("Fetch", GetLangText("Error-NotFound"), true)
151 | return 404, response.Header, nil
152 | }
153 |
154 | if allowCache && response.StatusCode == 304 {
155 | Log("Debug-Fetch", GetLangText("Debug-Request_NoChange"), false, url)
156 | return 304, response.Header, nil
157 | }
158 |
159 | if response.StatusCode != 200 {
160 | Log("Fetch", GetLangText("Error-UnknownStatusCode"), true, response.StatusCode)
161 | return response.StatusCode, response.Header, nil
162 | }
163 |
164 | if allowCache {
165 | etag := response.Header.Get("ETag")
166 |
167 | if etag != "" {
168 | urlETagCache[url] = etag
169 | }
170 |
171 | lastMod := response.Header.Get("Last-Modified")
172 | if lastMod != "" {
173 | urlLastModCache[url] = lastMod
174 | }
175 | }
176 |
177 | return response.StatusCode, response.Header, responseBody
178 | }
179 | func Submit(url string, postdata string, tryLogin bool, clientReq bool, withHeader *map[string]string) (int, http.Header, []byte) {
180 | request := NewRequest(true, url, postdata, clientReq, false, withHeader)
181 | if request == nil {
182 | return -1, nil, nil
183 | }
184 |
185 | var response *http.Response
186 | var err error
187 |
188 | if clientReq {
189 | response, err = httpClient.Do(request)
190 | } else {
191 | response, err = httpClientExternal.Do(request)
192 | }
193 |
194 | if err != nil {
195 | Log("Submit", GetLangText("Error-FetchResponse"), true, err.Error())
196 | return -2, nil, nil
197 | }
198 |
199 | responseBody, err := ioutil.ReadAll(response.Body)
200 | defer response.Body.Close()
201 |
202 | if err != nil {
203 | Log("Submit", GetLangText("Error-ReadResponse"), true, err.Error())
204 | return -3, nil, nil
205 | }
206 |
207 | if response.StatusCode == 204 {
208 | Log("Fetch", GetLangText("Debug-Request_NoContent"), false, url)
209 | return 204, response.Header, nil
210 | }
211 |
212 | if response.StatusCode == 401 {
213 | Log("Submit", GetLangText("Error-NoAuth"), true)
214 | return 401, response.Header, nil
215 | }
216 |
217 | if response.StatusCode == 403 {
218 | if tryLogin {
219 | Login()
220 | }
221 | Log("Submit", GetLangText("Error-Forbidden"), true)
222 | return 403, response.Header, nil
223 | }
224 |
225 | if response.StatusCode == 409 {
226 | // 尝试获取并设置 CSRF Token.
227 | if currentClientType == "Transmission" {
228 | trCSRFToken := response.Header.Get("X-Transmission-Session-Id")
229 | if trCSRFToken != "" {
230 | Tr_SetCSRFToken(trCSRFToken)
231 | return 409, response.Header, nil
232 | }
233 | }
234 |
235 | if tryLogin {
236 | Login()
237 | }
238 |
239 | Log("Fetch", GetLangText("Error-Forbidden"), true)
240 | return 409, response.Header, nil
241 | }
242 |
243 | if response.StatusCode == 404 {
244 | Log("Submit", GetLangText("Error-NotFound"), true)
245 | return 404, response.Header, nil
246 | }
247 |
248 | if response.StatusCode != 200 {
249 | Log("Submit", GetLangText("Error-UnknownStatusCode"), true, response.StatusCode)
250 | return response.StatusCode, response.Header, nil
251 | }
252 |
253 | return response.StatusCode, response.Header, responseBody
254 | }
255 |
--------------------------------------------------------------------------------
/client_BitComet.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "github.com/PuerkitoBio/goquery"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type BC_TorrentStruct struct {
11 | TotalSize int64
12 | UpSpeed int64
13 | }
14 | type BC_PeerStruct struct {
15 | IP string
16 | Port int
17 | Client string
18 | // PeerID string
19 | Progress float64
20 | Downloaded int64
21 | Uploaded int64
22 | DlSpeed int64
23 | UpSpeed int64
24 | }
25 |
26 | func BC_ParseTorrentLink(torrentLinkStr string) int {
27 | torrentIDSplit1 := strings.SplitN(StrTrim(torrentLinkStr), "?id=", 2)
28 | if len(torrentIDSplit1) < 2 {
29 | return -2
30 | }
31 |
32 | torrentIDStr := strings.SplitN(torrentIDSplit1[1], "&", 2)[0]
33 | torrentID, err := strconv.Atoi(torrentIDStr)
34 | if err != nil {
35 | return -3
36 | }
37 |
38 | return torrentID
39 | }
40 | func BC_ParseSize(sizeStr string) int64 {
41 | sizeStr = StrTrim(sizeStr)
42 | if sizeStr == "" {
43 | return 0
44 | }
45 |
46 | sizeStrSplit := strings.SplitN(sizeStr, " ", 2)
47 | if len(sizeStrSplit) < 2 || len(sizeStrSplit[1]) < 2 {
48 | return -1
49 | }
50 |
51 | rawSize, err := strconv.ParseFloat(sizeStrSplit[0], 64)
52 | if err != nil {
53 | return -2
54 | }
55 |
56 | matched := false
57 | multipler := 1
58 | switch strings.ToUpper(sizeStrSplit[1]) {
59 | case "EB":
60 | multipler *= 1024
61 | fallthrough
62 | case "PB":
63 | multipler *= 1024
64 | fallthrough
65 | case "TB":
66 | multipler *= 1024
67 | fallthrough
68 | case "GB":
69 | multipler *= 1024
70 | fallthrough
71 | case "MB":
72 | multipler *= 1024
73 | fallthrough
74 | case "KB":
75 | multipler *= 1024
76 | fallthrough
77 | case "B":
78 | matched = true
79 | }
80 |
81 | if !matched {
82 | return -3
83 | }
84 |
85 | return int64(rawSize * float64(multipler))
86 | }
87 | func BC_ParseSpeed(speedStr string) int64 {
88 | speedStr = StrTrim(speedStr)
89 | if speedStr == "" {
90 | return 0
91 | }
92 |
93 | speedStrSplit := strings.SplitN(speedStr, "/", 2)
94 |
95 | if len(speedStrSplit) < 2 || len(speedStrSplit[1]) != 1 {
96 | return -1
97 | }
98 |
99 | return BC_ParseSize(speedStrSplit[0])
100 | }
101 | func BC_ParsePrecent(precentStr string) float64 {
102 | precentStr = StrTrim(precentStr)
103 | if len(precentStr) < 2 {
104 | return -1
105 | }
106 |
107 | precentStr = precentStr[:(len(precentStr) - 1)]
108 | precent, err := strconv.ParseFloat(precentStr, 64)
109 | if err != nil {
110 | return -2
111 | }
112 |
113 | return precent
114 | }
115 | func BC_ParseIP(ipStr string) (string, int) {
116 | ipStr = strings.ToLower(StrTrim(ipStr))
117 | if ipStr == "myself" {
118 | return "", -1
119 | }
120 |
121 | lastColonIndex := strings.LastIndex(ipStr, ":")
122 | if lastColonIndex == -1 || len(ipStr) < (lastColonIndex+2) {
123 | return "", -2
124 | }
125 |
126 | ipWithoutPortStr := ipStr[:lastColonIndex]
127 | portStr := ipStr[(lastColonIndex + 1):]
128 | port, err := strconv.Atoi(portStr)
129 | if err != nil {
130 | return "", -3
131 | }
132 |
133 | return ipWithoutPortStr, port
134 | }
135 | func BC_DetectClient() bool {
136 | apiResponseStatusCode, apiResponseHeaders, _ := Fetch(config.ClientURL+"/panel/", false, false, false, nil)
137 | return (apiResponseStatusCode == 401 && strings.Contains(apiResponseHeaders.Get("WWW-Authenticate"), "BitComet"))
138 | }
139 | func BC_Login() bool {
140 | // BitComet 通过 Basic Auth 进行认证, 因此此处只进行验证.
141 | apiResponseStatusCode, _, _ := Fetch(config.ClientURL+"/panel/", false, true, false, nil)
142 | return (apiResponseStatusCode == 200)
143 | }
144 | func BC_FetchTorrents() *map[int]BC_TorrentStruct {
145 | _, _, torrentsResponseBody := Fetch(config.ClientURL+"/panel/task_list?group=active", true, true, false, nil)
146 | if torrentsResponseBody == nil {
147 | Log("FetchTorrents", GetLangText("Error"), true)
148 | return nil
149 | }
150 |
151 | document, err := goquery.NewDocumentFromReader(bytes.NewReader(torrentsResponseBody))
152 | if err != nil {
153 | Log("FetchTorrents", GetLangText("Error-Parse"), true, err.Error())
154 | return nil
155 | }
156 |
157 | torrentsMap := make(map[int]BC_TorrentStruct)
158 | document.Find("table").Last().Find("tbody > tr").Each(func(index int, element *goquery.Selection) {
159 | if index == 0 {
160 | return
161 | }
162 |
163 | torrentStatus := ""
164 | torrentID := 0
165 | var torrentSize int64 = -233
166 | var torrentUpSpeed int64 = -233
167 | element.Find("td").EachWithBreak(func(tdIndex int, tdElement *goquery.Selection) bool {
168 | switch tdIndex {
169 | case 0:
170 | if strings.ToUpper(StrTrim(tdElement.Text())) != "BT" {
171 | return false
172 | }
173 | case 1:
174 | href, exists := tdElement.Find("a").Attr("href")
175 | if !exists {
176 | return false
177 | }
178 |
179 | torrentID = BC_ParseTorrentLink(href)
180 | case 2:
181 | torrentStatus = strings.ToLower(StrTrim(tdElement.Text()))
182 | case 4:
183 | torrentSize = BC_ParseSize(tdElement.Text())
184 | case 7:
185 | torrentUpSpeed = BC_ParseSpeed(tdElement.Text())
186 | }
187 |
188 | return true
189 | })
190 |
191 | if torrentStatus == "" || torrentID <= 0 || torrentSize <= 0 || torrentUpSpeed < 0 {
192 | return
193 | }
194 |
195 | torrentsMap[torrentID] = BC_TorrentStruct{TotalSize: torrentSize, UpSpeed: torrentUpSpeed}
196 | })
197 |
198 | return &torrentsMap
199 | }
200 | func BC_FetchTorrentPeers(infoHash string) *[]BC_PeerStruct {
201 | _, _, torrentPeersResponseBody := Fetch(config.ClientURL+"/panel/task_detail?id="+infoHash+"&show=peers", true, true, false, nil)
202 | if torrentPeersResponseBody == nil {
203 | Log("FetchTorrentPeers", GetLangText("Error"), true)
204 | return nil
205 | }
206 |
207 | document, err := goquery.NewDocumentFromReader(bytes.NewReader(torrentPeersResponseBody))
208 | if err != nil {
209 | Log("FetchTorrentPeers", GetLangText("Error-Parse"), true, err.Error())
210 | return nil
211 | }
212 |
213 | torrentPeersMap := []BC_PeerStruct{}
214 | document.Find("table").Last().Find("tbody > tr").Each(func(index int, element *goquery.Selection) {
215 | if index == 0 {
216 | return
217 | }
218 |
219 | peerIP := ""
220 | peerPort := -233
221 | var peerProgress float64 = -233
222 | var peerDlSpeed int64 = -233
223 | var peerUpSpeed int64 = -233
224 | var peerDownloaded int64 = -233
225 | var peerUploaded int64 = -233
226 | peerClient := ""
227 | //peerID := ""
228 | element.Find("td").EachWithBreak(func(tdIndex int, tdElement *goquery.Selection) bool {
229 | switch tdIndex {
230 | case 0:
231 | peerIP, peerPort = BC_ParseIP(tdElement.Text())
232 | case 1:
233 | peerProgress = BC_ParsePrecent(tdElement.Text())
234 | case 2:
235 | peerDlSpeed = BC_ParseSpeed(tdElement.Text())
236 | case 3:
237 | peerUpSpeed = BC_ParseSpeed(tdElement.Text())
238 | case 4:
239 | peerDownloaded = BC_ParseSize(tdElement.Text())
240 | case 5:
241 | peerUploaded = BC_ParseSize(tdElement.Text())
242 | case 9:
243 | peerClient = tdElement.Text()
244 | case 10:
245 | // 错误的信息.
246 | // peerID = tdElement.Text()
247 | }
248 |
249 | return true
250 | })
251 |
252 | if peerIP == "" || peerPort < 0 || peerProgress < 0 || peerDlSpeed < 0 || peerUpSpeed < 0 || peerDownloaded < 0 || peerUploaded < 0 {
253 | return
254 | }
255 |
256 | peerStruct := BC_PeerStruct{IP: peerIP, Port: peerPort, Client: peerClient, Progress: peerProgress, Downloaded: peerDownloaded, Uploaded: peerUploaded, DlSpeed: peerDlSpeed, UpSpeed: peerUpSpeed}
257 | torrentPeersMap = append(torrentPeersMap, peerStruct)
258 | })
259 |
260 | return &torrentPeersMap
261 | }
262 |
--------------------------------------------------------------------------------
/client_Transmission.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "strconv"
7 | "strings"
8 | "time"
9 | )
10 |
11 | type Tr_RequestStruct struct {
12 | Method string `json:"method"`
13 | Args interface{} `json:"arguments"`
14 | }
15 | type Tr_ResponseStruct struct {
16 | Result string `json:"result"`
17 | }
18 | type Tr_TorrentsResponseStruct struct {
19 | Result string `json:"result"`
20 | Args Tr_TorrentsStruct `json:"arguments"`
21 | }
22 | type Tr_ArgsStruct struct {
23 | Field []string `json:"fields"`
24 | }
25 | type Tr_ArgTorrentsStruct struct {
26 | IDs []string `json:"ids"`
27 | Field []string `json:"fields"`
28 | }
29 | type Tr_SessionSetStruct struct {
30 | BlocklistEnabled bool `json:"blocklist-enabled"`
31 | BlocklistSize int `json:"blocklist-size"`
32 | BlocklistURL string `json:"blocklist-url"`
33 | }
34 | type Tr_TorrentsStruct struct {
35 | Torrents []Tr_TorrentStruct `json:"torrents"`
36 | }
37 | type Tr_TorrentStruct struct {
38 | InfoHash string `json:"hashString"`
39 | TotalSize int64 `json:"totalSize"`
40 | Private bool `json:"private"`
41 | Peers []Tr_PeerStruct `json:"peers"`
42 | }
43 | type Tr_PeerStruct struct {
44 | IP string `json:"address"`
45 | Port int `json:"port"`
46 | Client string `json:"clientName"`
47 | Progress float64 `json:"progress"`
48 | IsUploading bool `json:"isUploadingTo"`
49 | DlSpeed int64 `json:"rateToClient"`
50 | UpSpeed int64 `json:"rateToPeer"`
51 | }
52 |
53 | var Tr_csrfToken = ""
54 | var Tr_ipfilterStr = ""
55 | var Tr_jsonHeader = map[string]string{"Content-Type": "application/json"}
56 |
57 | func Tr_InitClient() {
58 | go StartServer()
59 | }
60 | func Tr_ProcessHTTP(w http.ResponseWriter, r *http.Request) bool {
61 | if strings.SplitN(r.RequestURI, "?", 2)[0] == "/ipfilter.dat" {
62 | w.WriteHeader(200)
63 | w.Write([]byte(Tr_ipfilterStr))
64 |
65 | return true
66 | }
67 |
68 | return false
69 | }
70 | func Tr_SetURL() bool {
71 | return false
72 | }
73 | func Tr_DetectVersion() bool {
74 | detectJSON, err := json.Marshal(Tr_RequestStruct{Method: "session-get", Args: Tr_ArgsStruct{Field: []string{"version"}}})
75 | if err != nil {
76 | Log("DetectVersion", GetLangText("Error-GenJSON"), true, err.Error())
77 | return false
78 | }
79 |
80 | detectStatusCode, _, _ := Submit(config.ClientURL, string(detectJSON), false, false, &Tr_jsonHeader)
81 | return (detectStatusCode == 200 || detectStatusCode == 409)
82 | }
83 | func Tr_Login() bool {
84 | // Transmission 通过 Basic Auth 进行认证, 因此实际处理 CSRF 请求以避免 409 响应.
85 | loginJSON, err := json.Marshal(Tr_RequestStruct{Method: "session-get"})
86 | if err != nil {
87 | Log("Login", GetLangText("Error-GenJSON"), true, err.Error())
88 | return false
89 | }
90 |
91 | Submit(config.ClientURL, string(loginJSON), false, true, nil)
92 |
93 | if Tr_csrfToken == "" {
94 | Log("Login", GetLangText("Error-Login"), true)
95 | return false
96 | }
97 |
98 | return true
99 | }
100 | func Tr_SetCSRFToken(csrfToken string) {
101 | Tr_csrfToken = csrfToken
102 | Log("SetCSRFToken", GetLangText("Success-SetCSRFToken"), true, csrfToken)
103 | }
104 | func Tr_FetchTorrents() *Tr_TorrentsStruct {
105 | loginJSON, err := json.Marshal(Tr_RequestStruct{Method: "torrent-get", Args: Tr_ArgsStruct{Field: []string{"hashString", "totalSize", "isPrivate", "peers"}}})
106 | if err != nil {
107 | Log("FetchTorrents", GetLangText("Error-GenJSON"), true, err.Error())
108 | return nil
109 | }
110 |
111 | _, _, torrentsResponseBody := Submit(config.ClientURL, string(loginJSON), true, true, &Tr_jsonHeader)
112 | if torrentsResponseBody == nil {
113 | Log("FetchTorrents", GetLangText("Error"), true)
114 | return nil
115 | }
116 |
117 | var torrentsResponse Tr_TorrentsResponseStruct
118 | if err := json.Unmarshal(torrentsResponseBody, &torrentsResponse); err != nil {
119 | Log("FetchTorrents", GetLangText("Error-Parse"), true, err.Error())
120 | return nil
121 | }
122 |
123 | if torrentsResponse.Result != "success" {
124 | Log("FetchTorrents", GetLangText("Error-Parse"), true, torrentsResponse.Result)
125 | return nil
126 | }
127 |
128 | return &torrentsResponse.Args
129 | }
130 |
131 | func Tr_RestartTorrentByMap(blockPeerMap map[string]BlockPeerInfoStruct) {
132 | peerInfoHashes := []string{}
133 | for _, peerInfo := range blockPeerMap {
134 | peerInfoHashes = append(peerInfoHashes, peerInfo.InfoHash)
135 | }
136 |
137 | if len(peerInfoHashes) <= 0 {
138 | return
139 | }
140 |
141 | stopJSON, err := json.Marshal(Tr_RequestStruct{Method: "torrent-stop", Args: Tr_ArgTorrentsStruct{IDs: peerInfoHashes}})
142 | if err != nil {
143 | Log("RestartTorrentByMap", GetLangText("Error-GenJSON"), true, err.Error())
144 | return
145 | }
146 |
147 | stopStatusCode, _, _ := Submit(config.ClientURL, string(stopJSON), true, true, &Tr_jsonHeader)
148 | if stopStatusCode != 200 {
149 | Log("RestartTorrentByMap", GetLangText("Error-RestartTorrentByMap_Stop"), true, err.Error())
150 | return
151 | }
152 |
153 | Log("RestartTorrentByMap", GetLangText("Debug-RestartTorrentByMap_Wait"), true, config.Interval)
154 | time.Sleep(time.Duration(config.RestartInterval) * time.Second)
155 |
156 | startJSON, err := json.Marshal(Tr_RequestStruct{Method: "torrent-start", Args: Tr_ArgTorrentsStruct{IDs: peerInfoHashes}})
157 | if err != nil {
158 | Log("RestartTorrentByMap", GetLangText("Error-GenJSON"), true, err.Error())
159 | return
160 | }
161 |
162 | startStatusCode, _, _ := Submit(config.ClientURL, string(startJSON), true, true, &Tr_jsonHeader)
163 | if startStatusCode != 200 {
164 | Log("RestartTorrentByMap", GetLangText("Error-RestartTorrentByMap_Start"), true, err.Error())
165 | return
166 | }
167 | }
168 | func Tr_SubmitBlockPeer(blockPeerMap map[string]BlockPeerInfoStruct) bool {
169 | ipfilterCount, ipfilterStr := GenIPFilter(2, blockPeerMap)
170 | Tr_ipfilterStr = ipfilterStr
171 |
172 | blocklistURL := ""
173 | if strings.Contains(config.Listen, ".") {
174 | blocklistURL = "http://" + config.Listen
175 | } else {
176 | blocklistURL = "http://127.0.0.1" + config.Listen
177 | }
178 | blocklistURL += "/ipfilter.dat?t=" + strconv.FormatInt(currentTimestamp, 10)
179 |
180 | sessionSetJSON, err := json.Marshal(Tr_RequestStruct{Method: "session-set", Args: Tr_SessionSetStruct{BlocklistEnabled: true, BlocklistSize: ipfilterCount, BlocklistURL: blocklistURL}})
181 | if err != nil {
182 | Log("SubmitBlockPeer", GetLangText("Error-GenJSON"), true, err.Error())
183 | return false
184 | }
185 |
186 | _, _, sessionResponseBody := Submit(config.ClientURL, string(sessionSetJSON), true, true, &Tr_jsonHeader)
187 | if sessionResponseBody == nil {
188 | Log("SubmitBlockPeer", GetLangText("Error"), true)
189 | return false
190 | }
191 |
192 | var sessionResponse Tr_ResponseStruct
193 | if err := json.Unmarshal(sessionResponseBody, &sessionResponse); err != nil {
194 | Log("SubmitBlockPeer", GetLangText("Error-Parse"), true, err.Error())
195 | return false
196 | }
197 |
198 | if sessionResponse.Result != "success" {
199 | Log("SubmitBlockPeer", GetLangText("Error-Parse"), true, sessionResponse.Result)
200 | return false
201 | }
202 |
203 | blocklistUpdateJSON, err := json.Marshal(Tr_RequestStruct{Method: "blocklist-update"})
204 | if err != nil {
205 | Log("SubmitBlockPeer", GetLangText("Error-GenJSON"), true, err.Error())
206 | return false
207 | }
208 |
209 | _, _, blocklistUpdateResponseBody := Submit(config.ClientURL, string(blocklistUpdateJSON), true, true, &Tr_jsonHeader)
210 | if blocklistUpdateResponseBody == nil {
211 | Log("SubmitBlockPeer", GetLangText("Error"), true)
212 | return false
213 | }
214 |
215 | var blocklistUpdateResponse Tr_ResponseStruct
216 | if err := json.Unmarshal(blocklistUpdateResponseBody, &blocklistUpdateResponse); err != nil {
217 | Log("SubmitBlockPeer", GetLangText("Error-Parse"), true, err.Error())
218 | return false
219 | }
220 |
221 | if blocklistUpdateResponse.Result != "success" {
222 | Log("SubmitBlockPeer", GetLangText("Error-Parse"), true, blocklistUpdateResponse.Result)
223 | return false
224 | }
225 |
226 | Tr_RestartTorrentByMap(blockPeerMap)
227 |
228 | return true
229 | }
230 |
--------------------------------------------------------------------------------
/lang/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "ProgramVersion": "Program version",
3 | "ConfigPath": "Config path",
4 | "DebugMode": "Debug mode",
5 | "StartDelay": "Start delay",
6 | "NoChdir": "Don't change working directory",
7 | "RegHotkey": "Register window hotkey",
8 | "HideWindow": "Hide window by default (Only Windows)",
9 | "HideSystray": "Hide systray by default (Only Windows)",
10 | "RunConsole_StartDelay": "Start delay: %d Sec",
11 | "RunConsole_ProgramHasStarted": "Program has started",
12 | "LoadInitConfig_DetectClientFailed": "Detect client failed",
13 | "LoadInitConfig_AuthFailed": "Authentication failed",
14 | "Task_BanInfo": "Ban Client (This time): %d, Ban Client (Current): %d",
15 | "Task_BanInfoWithIP": "Ban Client (This time): %d, Ban IP (This time): %d, Ban IP/Client (Current): %d",
16 | "GC_IPMap": "Trigger GC (ipMap): %d",
17 | "GC_TorrentMap": "Trigger GC (torrentMap): %s/%d",
18 | "ReqStop_Stoping": "Program is stopping..",
19 | "Stop_CaughtPanic": "Caught program panic: %s",
20 | "Stop_StacktraceWhenPanic": "Stacktrace when panic: %s",
21 | "GetClientConfig_UseConfig": "Use client config: %s",
22 | "LoadLog_HotReload": "Log directory change found, hot reload in progress (%s)",
23 | "SyncWithServer_Compile-BlockByReason": "Ban reason: %s",
24 | "CheckUpdate-ShowVersion": "Current version: %s, Latest version: %s, Latest version (Beta): %s",
25 | "CheckUpdate-DetectNewVersion": "Detected new version: %s, you can manual update it manually by visit %s, update content are below: \n%s",
26 | "CheckUpdate-DetectNewBetaVersion": "Detected new version (Beta): %s, you can update it manually by visit %s, update content are below: \n%s",
27 | "CheckUpdate-Ignore_UnknownVersion": "Skip auto check update: Unknwon version",
28 | "CheckUpdate-Ignore_NightlyVersion": "Skip auto check update: Nightly version",
29 | "CheckUpdate-Ignore_BadVersion": "Skip auto check update: Error version %s",
30 | "GetProxy_ProxyFound": "%s proxy found: %s (Source: %s)",
31 | "GetProxy_ProxyNotFound": "%s proxy not found",
32 | "GetProxy_UseEnvVar": "The current platform sets the proxy through environment variables. If you want to use it, please make sure that the environment variables are set correctly",
33 | "ClientQB_Detect-OldClientURL": "Detected ClientURL (Web UI), automatically changed to ClientURL (Web API): %s",
34 | "Debug-LoadConfig_HotReload": "Config (%s) change found, hot reload in progress",
35 | "Debug-Request_NoChange": "The requested URL (%s) has not changed",
36 | "Debug-Request_NoContent": "The requested URL (%s) did not return any content",
37 | "Debug-SetBlockListFromFile_HotReload": "BlockListFile (%s) change found, hot reload in progress",
38 | "Debug-SetIPBlockListFromFile_HotReload": "IPBlockListFile (%s) change found, hot reload in progress",
39 | "Debug-ShowOrHiddenWindow_HideWindow": "Hide window",
40 | "Debug-ShowOrHiddenWindow_ShowWindow": "Show window",
41 | "Debug-RestartTorrentByMap_Wait": "Wait interval before resuming torrent: %d Sec",
42 | "Abandon-SetURL": "Abandon reading client config file (WebUIEnabled: %t, Address: %s)",
43 | "Error": "An error has occurred",
44 | "Error-LoadLang": "An error occurred while loading language file: %s",
45 | "Error-ReadLang": "An error occurred while reading language file: %s|%s",
46 | "Error-ParseLang": "An error occurred while parsing language file: %s|%s",
47 | "Error-RegHotkey": "An error occurred while registering window hotkey: %s",
48 | "Error-DetectProgramPath": "An error occurred while detecting program execution path: %s",
49 | "Error-LoadConfigMeta": "An error occurred while loading config (%s) metadata: %s",
50 | "Error-LoadConfig": "An error occurred while loading config (%s): %s",
51 | "Error-ParseConfig": "An error occurred while parsing config (%s): %s",
52 | "Error-LoadFile": "An error occurred while parsing file (%s): %s",
53 | "Error-GetClientConfig_LoadConfig": "An error occurred while loading client config file: %s",
54 | "Error-GetClientConfig_LoadConfigMeta": "An error occurred while reading client config file: %s",
55 | "Error-SetBlocklistFromContent_Compile": ":%d regexp %s has error (Source: %s)",
56 | "Error-SetIPBlockListFromContent_Compile": ":%d IP %s has error (Source: %s)",
57 | "Error-SyncWithServer_Compile": ":%d IP %s has error",
58 | "Error-RestartTorrentByMap_Stop": "An error occurred while stop torrent: %s",
59 | "Error-RestartTorrentByMap_Start": "An error occurred while start torrent: %s",
60 | "Error-LargeFile": "An error occurred while parsing: Target size is greater than 8MB",
61 | "Error-NewRequest": "An error occurred while requesting: %s",
62 | "Error-FetchResponse": "An error occurred while fetching: %s",
63 | "Error-FetchResponse2": "An error occurred while fetching",
64 | "Error-ReadResponse": "An error occurred while reading: %s",
65 | "Error-NoAuth": "An error occurred while requesting: Authentication failed",
66 | "Error-Forbidden": "An error occurred while requesting: Forbidden",
67 | "Error-NotFound": "An error occurred while requesting: Resource not found",
68 | "Error-UnknownStatusCode": "An error occurred while requesting: Unknown status code %d",
69 | "Error-Parse": "An error occurred while parsing: %s",
70 | "Error-Login": "An error occurred while logging in",
71 | "Error-FetchUpdate": "An error occurred while fetching update",
72 | "Error-GenJSON": "An error occurred while generate JSON: %s",
73 | "Error-GenJSONWithID": "An error occurred while generate JSON (%s): %s",
74 | "Error-Log_Write": "An error occurred while writing to log: %s",
75 | "Error-IPFilter_Write": "An error occurred while writing to IPFilter: %s",
76 | "Error-LoadLog_Mkdir": "An error occurred while creating log directory: %s",
77 | "Error-LoadLog_Close": "An error occurred while closing log: %s",
78 | "Error-RegexpMatchErr": "An error occurred while matching regexp: %s",
79 | "Error-Task_EmptyURL": "Detected ClientURL is empty, may not be configured and failed to automatically read client config file",
80 | "Error-Task_NotSupportClient": "Detected unsupport client, may not be configured and failed to automatically detect client type: %s",
81 | "Error-SyncWithServer_ServerError": "Sync server error: %s",
82 | "Error-Debug-EmptyLine": ":%d is empty",
83 | "Error-Debug-EmptyLineWithSource": ":%d is empty (%s)",
84 | "Error-Debug-GetClientConfigPath_GetUserHomeDir": "An error occurred while retrieving the User Home directory: %s",
85 | "Error-Debug-GetClientConfigPath_GetUserConfigDir": "An error occurred while retrieving the User Config directory: %s",
86 | "Failed-LoadInitConfig": "Failed to read config file or config file is incomplete",
87 | "Failed-ChangeWorkingDir": "Failed to change working directory: %s",
88 | "Failed-Login_BadUsernameOrPassword": "Login failed: Wrong username or password",
89 | "Failed-Login_Other": "Login failed: %s",
90 | "Failed-ExecCommand": "Exec command failed, output: %s, error: %s",
91 | "Success-RegHotkey": "Registered and started listening for window hotkey: CTRL+ALT+B",
92 | "Success-ChangeWorkingDir": "Change working directory: %s",
93 | "Success-LoadConfig": "Loading config (%s) successfully",
94 | "Success-SetCSRFToken": "Set CSRF Token successfully: %s",
95 | "Success-SetURL": "Read client config file successfully (WebUIEnabled: %t, URL: %s, Username: %s)",
96 | "Success-GenIPFilter": "%d IP rules are generate",
97 | "Success-SetBlocklistFromURL": "This time %d regexp rules are set (Source: BlockListURL)",
98 | "Success-SetIPBlockListFromURL": "This time %d IP rules are set (Source: IPBlockListURL)",
99 | "Success-SetBlockListFromFile": "This time %d regexp rules are set (Source: BlockListFile)",
100 | "Success-SetIPBlockListFromFile": "This time %d IP rules are set (Source: IPBlockListFile)",
101 | "Success-DetectClient": "Detect client type successful: %s",
102 | "Success-Login": "Login successful",
103 | "Success-ClearBlockPeer": "Cleaned up expired client: %d",
104 | "Success-ExecCommand": "Exec command success, output: %s",
105 | "Success-SyncWithServer": "Sync with server success",
106 |
107 | // Part ShadowBan.
108 | "Warning-ShadowBanAPINotExist": "ShadowBan API not detected, and the normal method will be used",
109 | "Warning-ShadowBanAPIDisabled": "ShadowBan API is disabled, and the normal method will be used",
110 | "Failed-UnknownShadowBanAPI": "Detected unknown ShadowBan API, and the normal method will be used",
111 | "Failed-GetQBPreferences": "Failed to get qBittorrent preferences"
112 | }
113 |
--------------------------------------------------------------------------------
/torrent.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "strings"
6 | "time"
7 | )
8 |
9 | type TorrentInfoStruct struct {
10 | Size int64
11 | Peers map[string]PeerInfoStruct
12 | }
13 | type PeerInfoStruct struct {
14 | Net *net.IPNet
15 | Port map[int]bool
16 | Progress float64
17 | Downloaded int64
18 | Uploaded int64
19 | }
20 |
21 | var torrentMap = make(map[string]TorrentInfoStruct)
22 | var lastTorrentMap = make(map[string]TorrentInfoStruct)
23 | var lastTorrentCleanTimestamp int64 = 0
24 |
25 | func AddTorrentInfo(torrentInfoHash string, torrentTotalSize int64, cidr *net.IPNet, peerIP string, peerPort int, peerProgress float64, peerUploaded int64) {
26 | if !((config.IPUploadedCheck && config.IPUpCheckPerTorrentRatio > 0) || config.BanByRelativeProgressUploaded || config.SyncServerURL != "") {
27 | return
28 | }
29 |
30 | var peers map[string]PeerInfoStruct
31 | var peerPortMap map[int]bool
32 | if torrentInfo, exist := torrentMap[torrentInfoHash]; !exist {
33 | peers = make(map[string]PeerInfoStruct)
34 | peerPortMap = make(map[int]bool)
35 | } else {
36 | peers = torrentInfo.Peers
37 | if peerInfo, exist := peers[peerIP]; !exist {
38 | peerPortMap = make(map[int]bool)
39 | } else {
40 | peerPortMap = peerInfo.Port
41 |
42 | // 防止 Peer 在周期内以重新连接的方式清空实际上传量.
43 | if peerInfo.Uploaded > peerUploaded {
44 | peerUploaded += peerInfo.Uploaded
45 | }
46 | }
47 | }
48 | peerPortMap[peerPort] = true
49 |
50 | peers[peerIP] = PeerInfoStruct{Net: cidr, Port: peerPortMap, Progress: peerProgress, Uploaded: peerUploaded}
51 | torrentMap[torrentInfoHash] = TorrentInfoStruct{Size: torrentTotalSize, Peers: peers}
52 | }
53 | func IsProgressNotMatchUploaded(torrentTotalSize int64, clientProgress float64, clientUploaded int64) bool {
54 | if config.BanByProgressUploaded && torrentTotalSize > 0 && clientProgress >= 0 && clientUploaded > 0 {
55 | /*
56 | 条件 1. 若客户端对 Peer 上传已大于等于 Torrnet 大小的 2%;
57 | 条件 2. 但 Peer 报告进度乘以下载量再乘以一定防误判倍率, 却比客户端上传量还小;
58 | 若满足以上条件, 则认为 Peer 是有问题的.
59 | e.g.:
60 | 若 torrentTotalSize: 100GB, clientProgress: 1% (0.01), clientUploaded: 6GB, config.BanByPUStartPrecent: 2 (0.02), config.BanByPUAntiErrorRatio: 5;
61 | 判断条件 1:
62 | torrentTotalSize * config.BanByPUStartPrecent = 100GB * 0.02 = 2GB, clientUploaded = 6GB >= 2GB
63 | 满足此条件;
64 | 判断条件 2:
65 | torrentTotalSize * clientProgress * config.BanByPUAntiErrorRatio = 100GB * 0.01 * 5 = 5GB, 5GB < clientUploaded = 6GB
66 | 满足此条件;
67 | 则该 Peer 将被封禁, 由于其报告进度为 1%, 算入 config.BanByPUAntiErrorRatio 滞后防误判倍率后为 5% (5GB), 但客户端实际却已上传 6GB.
68 | */
69 | startUploaded := (float64(torrentTotalSize) * (config.BanByPUStartPrecent / 100))
70 | peerReportDownloaded := (float64(torrentTotalSize) * clientProgress)
71 | if (clientUploaded/1024/1024) >= int64(config.BanByPUStartMB) && float64(clientUploaded) >= startUploaded && (peerReportDownloaded*config.BanByPUAntiErrorRatio) < float64(clientUploaded) {
72 | return true
73 | }
74 | }
75 | return false
76 | }
77 | func IsProgressNotMatchUploaded_Relative(torrentTotalSize int64, peerInfo PeerInfoStruct, lastPeerInfo PeerInfoStruct) int64 {
78 | // 若客户端对 Peer 上传已大于 0, 且相对上传量大于起始上传量, 则继续判断.
79 | var relativeUploaded int64 = (peerInfo.Uploaded - lastPeerInfo.Uploaded)
80 |
81 | if torrentTotalSize > 0 && peerInfo.Uploaded > 0 && (float64(relativeUploaded)/1024/1024) > float64(config.BanByRelativePUStartMB) {
82 | relativeUploadedPrecent := (1 - (float64(lastPeerInfo.Uploaded) / float64(peerInfo.Uploaded)))
83 | // 若相对上传百分比大于起始百分比, 则继续判断.
84 | if relativeUploadedPrecent > (config.BanByRelativePUStartPrecent / 100) {
85 | // 若相对上传百分比大于 Peer 报告进度乘以一定防误判倍率, 则认为 Peer 是有问题的.
86 | var peerReportProgress float64 = 0
87 | if peerInfo.Progress > 0 {
88 | peerReportProgress = (1 - (lastPeerInfo.Progress / peerInfo.Progress))
89 | }
90 | if relativeUploadedPrecent > (peerReportProgress * config.BanByRelativePUAntiErrorRatio) {
91 | return relativeUploaded
92 | }
93 | }
94 | }
95 | return 0
96 | }
97 | func CheckAllTorrent(torrentMap map[string]TorrentInfoStruct, lastTorrentMap map[string]TorrentInfoStruct) (int, int) {
98 | if ((config.IPUploadedCheck && config.IPUpCheckPerTorrentRatio > 0) || config.BanByRelativeProgressUploaded) && len(lastTorrentMap) > 0 && currentTimestamp > (lastTorrentCleanTimestamp+int64(config.TorrentMapCleanInterval)) {
99 | blockCount := 0
100 | ipBlockCount := 0
101 |
102 | for torrentInfoHash, torrentInfo := range torrentMap {
103 | for peerIP, peerInfo := range torrentInfo.Peers {
104 | if IsBlockedPeer(peerIP, -1, true) {
105 | continue
106 | }
107 |
108 | if config.IPUploadedCheck && config.IPUpCheckPerTorrentRatio > 0 {
109 | if float64(peerInfo.Uploaded) > (float64(torrentInfo.Size) * peerInfo.Progress * config.IPUpCheckPerTorrentRatio) {
110 | Log("CheckAllTorrent_AddBlockPeer (Torrent-Too high uploaded)", "%s (Uploaded: %.2f MB)", true, peerIP, (float64(peerInfo.Uploaded) / 1024 / 1024))
111 | ipBlockCount++
112 | AddBlockPeer("CheckAllTorrent", "Torrent-Too high uploaded", peerIP, -1, torrentInfoHash)
113 | AddBlockCIDR(peerIP, peerInfo.Net)
114 | continue
115 | }
116 | }
117 |
118 | if config.BanByRelativeProgressUploaded {
119 | if lastPeerInfo, exist := lastTorrentMap[torrentInfoHash].Peers[peerIP]; exist {
120 | if uploadDuring := IsProgressNotMatchUploaded_Relative(torrentInfo.Size, peerInfo, lastPeerInfo); uploadDuring > 0 {
121 | for port := range peerInfo.Port {
122 | if IsBlockedPeer(peerIP, port, true) {
123 | continue
124 | }
125 | Log("CheckAllTorrent_AddBlockPeer (Bad-Relative_Progress_Uploaded)", "%s:%d (UploadDuring: %.2f MB)", true, peerIP, port, uploadDuring)
126 | blockCount++
127 | AddBlockPeer("CheckAllTorrent", "Bad-Relative_Progress_Uploaded", peerIP, port, torrentInfoHash)
128 | AddBlockCIDR(peerIP, peerInfo.Net)
129 | }
130 | continue
131 | }
132 | }
133 | }
134 | }
135 | }
136 |
137 | lastTorrentCleanTimestamp = currentTimestamp
138 | DeepCopyTorrentMap(torrentMap, lastTorrentMap)
139 |
140 | return blockCount, ipBlockCount
141 | }
142 |
143 | return 0, 0
144 | }
145 | func CheckTorrent(torrentInfoHash string, torrentTracker string, torrentLeecherCount int64, torrentPeers interface{}) (int, interface{}) {
146 | if torrentInfoHash == "" {
147 | return -1, nil
148 | }
149 |
150 | if config.IgnorePTTorrent && torrentTracker != "" {
151 | if torrentTracker == "Private" {
152 | return -4, nil
153 | }
154 |
155 | lowerTorrentTracker := strings.ToLower(torrentTracker)
156 | if strings.Contains(lowerTorrentTracker, "?passkey=") || strings.Contains(lowerTorrentTracker, "?authkey=") || strings.Contains(lowerTorrentTracker, "?secure=") {
157 | return -4, nil
158 | }
159 |
160 | randomStrMatched, err := randomStrRegexp.MatchString(lowerTorrentTracker)
161 | if err != nil {
162 | Log("CheckTorrent_MatchTracker", GetLangText("Error-MatchRegexpErr"), true, err.Error())
163 | } else if randomStrMatched {
164 | return -4, nil
165 | }
166 | }
167 |
168 | if config.IgnoreNoLeechersTorrent && torrentLeecherCount <= 0 {
169 | return -2, nil
170 | }
171 |
172 | if torrentPeers != nil {
173 | return 0, torrentPeers
174 | }
175 |
176 | torrentPeers = FetchTorrentPeers(torrentInfoHash)
177 | if torrentPeers == nil {
178 | return -3, nil
179 | }
180 |
181 | return 0, torrentPeers
182 | }
183 | func ProcessTorrent(torrentInfoHash string, torrentTracker string, torrentLeecherCount int64, torrentTotalSize int64, torrentPeers interface{}, emptyHashCount *int, noLeechersCount *int, badTorrentInfoCount *int, ptTorrentCount *int, blockCount *int, ipBlockCount *int, badPeersCount *int, emptyPeersCount *int) {
184 | torrentInfoHash = strings.ToLower(torrentInfoHash)
185 | torrentStatus, torrentPeersStruct := CheckTorrent(torrentInfoHash, torrentTracker, torrentLeecherCount, torrentPeers)
186 | if config.Debug_CheckTorrent {
187 | Log("Debug-CheckTorrent", "%s (Status: %d)", false, torrentInfoHash, torrentStatus)
188 | }
189 |
190 | skipSleep := false
191 |
192 | switch torrentStatus {
193 | case -1:
194 | skipSleep = true
195 | *emptyHashCount++
196 | case -2:
197 | skipSleep = true
198 | *noLeechersCount++
199 | case -3:
200 | *badTorrentInfoCount++
201 | case -4:
202 | skipSleep = true
203 | *ptTorrentCount++
204 | case 0:
205 | switch currentClientType {
206 | case "qBittorrent":
207 | torrentPeers := torrentPeersStruct.(*qB_TorrentPeersStruct).Peers
208 | for _, peer := range torrentPeers {
209 | ProcessPeer(peer.IP, peer.Port, peer.PeerID, peer.Client, peer.DlSpeed, peer.UpSpeed, peer.Progress, peer.Downloaded, peer.Uploaded, torrentInfoHash, torrentTotalSize, blockCount, ipBlockCount, badPeersCount, emptyPeersCount)
210 | }
211 | case "Transmission":
212 | torrentPeers := torrentPeersStruct.([]Tr_PeerStruct)
213 | for _, peer := range torrentPeers {
214 | // Transmission 目前似乎并不提供 Peer 的 PeerID 及 Downloaded/Uploaded, 因此使用无效值取代.
215 | ProcessPeer(peer.IP, peer.Port, "", peer.Client, peer.DlSpeed, peer.UpSpeed, peer.Progress, -1, -1, torrentInfoHash, torrentTotalSize, blockCount, ipBlockCount, badPeersCount, emptyPeersCount)
216 | }
217 | case "BitComet":
218 | torrentPeers := torrentPeersStruct.(*[]BC_PeerStruct)
219 | for _, peer := range *torrentPeers {
220 | // BitComet 目前不为其支持 PeerID, 因此使用无效值取代.
221 | ProcessPeer(peer.IP, peer.Port, "", peer.Client, peer.DlSpeed, peer.UpSpeed, peer.Progress, peer.Downloaded, peer.Uploaded, torrentInfoHash, torrentTotalSize, blockCount, ipBlockCount, badPeersCount, emptyPeersCount)
222 | }
223 | }
224 | }
225 |
226 | if !skipSleep && config.SleepTime != 0 {
227 | time.Sleep(time.Duration(config.SleepTime) * time.Millisecond)
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/i18n.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 |
7 | "github.com/Xuanwo/go-locale"
8 | "github.com/tidwall/jsonc"
9 | )
10 |
11 | var langContent map[string]string
12 | var defaultLangContent = map[string]string{
13 | "ProgramVersion": "程序版本",
14 | "ConfigPath": "配置文件路径",
15 | "AdditionalConfigPath": "附加配置文件路径",
16 | "DebugMode": "调试模式",
17 | "StartDelay": "启动延迟",
18 | "NoChdir": "不切换工作目录",
19 | "RegHotkey": "注册窗口热键",
20 | "HideWindow": "默认隐藏窗口 (仅 Windows)",
21 | "HideSystray": "默认隐藏托盘图标 (仅 Windows)",
22 | "RunConsole_StartDelay": "启动延迟: %d 秒",
23 | "RunConsole_ProgramHasStarted": "程序已启动",
24 | "LoadInitConfig_DetectClientFailed": "检测客户端失败",
25 | "LoadInitConfig_AuthFailed": "认证失败",
26 | "Task_BanInfo": "此次封禁客户端: %d 个, 当前封禁客户端: %d 个",
27 | "Task_BanInfoWithIP": "此次封禁客户端: %d 个, 此次封禁 IP: %d 个, 当前封禁 IP 及客户端: %d 个",
28 | "GC_IPMap": "触发垃圾回收 (ipMap): %d",
29 | "GC_TorrentMap": "触发垃圾回收 (torrentMap): %s/%d",
30 | "ReqStop_Stoping": "程序正在停止..",
31 | "Stop_CaughtPanic": "捕获到程序崩溃: %s",
32 | "Stop_StacktraceWhenPanic": "崩溃时的堆栈信息: %s",
33 | "GetClientConfig_UseConfig": "使用客户端配置文件: %s",
34 | "LoadLog_HotReload": "发现日志目录更改, 正在进行热重载 (%s)",
35 | "SyncWithServer_Compile-BlockByReason": "封禁原因: %s",
36 | "CheckUpdate-ShowVersion": "当前版本: %s, 最新版本: %s, 最新版本 (Beta): %s",
37 | "CheckUpdate-DetectNewVersion": "检测到新的版本: %s, 可访问 %s 手动进行更新, 更新内容如下: \n%s",
38 | "CheckUpdate-DetectNewBetaVersion": "检测到新的版本 (Beta): %s, 可访问 %s 手动进行更新, 更新内容如下: \n%s",
39 | "CheckUpdate-Ignore_UnknownVersion": "跳过自动检查更新: 未知版本",
40 | "CheckUpdate-Ignore_NightlyVersion": "跳过自动检查更新: 夜间构建版本",
41 | "CheckUpdate-Ignore_BadVersion": "跳过自动检查更新: 错误版本 %s",
42 | "GetProxy_ProxyFound": "发现 %s 代理: %s (来源: %s)",
43 | "GetProxy_ProxyNotFound": "未能发现 %s 代理",
44 | "GetProxy_UseEnvVar": "当前平台通过环境变量设置代理, 若要使用, 请确保已正确设置环境变量",
45 | "ClientQB_Detect-OldClientURL": "检测到 ClientURL (Web UI), 已自动修改至 ClientURL (Web API): %s",
46 | "Debug-LoadConfig_HotReload": "发现配置文件 (%s) 更改, 正在进行热重载",
47 | "Debug-Request_NoChange": "请求的 URL (%s) 没有发生改变",
48 | "Debug-Request_NoContent": "请求的 URL (%s) 没有返回内容",
49 | "Debug-SetBlockListFromFile_HotReload": "发现 BlockListFile (%s) 更改, 正在进行热重载",
50 | "Debug-SetIPBlockListFromFile_HotReload": "发现 IPBlockListFile (%s) 更改, 正在进行热重载",
51 | "Debug-ShowOrHiddenWindow_HideWindow": "窗口隐藏",
52 | "Debug-ShowOrHiddenWindow_ShowWindow": "窗口显示",
53 | "Debug-RestartTorrentByMap_Wait": "重新开始 Torrent 前的等待间隔: %d 秒",
54 | "Abandon-SetURL": "放弃读取客户端配置文件 (WebUIEnabled: %t, Address: %s)",
55 | "Error": "发生错误",
56 | "Error-LoadLang": "加载语言文件时发生了错误: %s",
57 | "Error-ReadLang": "读取语言文件时发生了错误: %s|%s",
58 | "Error-ParseLang": "解析语言文件时发生了错误: %s|%s",
59 | "Error-RegHotkey": "注册窗口热键时发生错误: %v",
60 | "Error-DetectProgramPath": "检测程序运行路径时发生了错误: %s",
61 | "Error-LoadConfigMeta": "加载配置文件 (%s) 元数据时发生了错误: %s",
62 | "Error-LoadConfig": "加载配置文件 (%s) 时发生了错误: %s",
63 | "Error-ParseConfig": "解析配置文件 (%s) 时发生了错误: %s",
64 | "Error-LoadFile": "加载文件 (%s) 时发生了错误: %s",
65 | "Error-GetClientConfig_LoadConfig": "加载客户端配置文件时发生了错误: %s",
66 | "Error-GetClientConfig_LoadConfigMeta": "读取客户端配置文件元数据时发生了错误: %s",
67 | "Error-SetBlockListFromContent_Compile": ":%d 表达式 %s 有错误 (来源: %s)",
68 | "Error-SetIPBlockListFromContent_Compile": ":%d IP %s 有错误 (来源: %s)",
69 | "Error-SyncWithServer_Compile": ":%d IP %s 有错误",
70 | "Error-RestartTorrentByMap_Stop": "停止 Torrent 时发生了错误: %s",
71 | "Error-RestartTorrentByMap_Start": "开始 Torrent 时发生了错误: %s",
72 | "Error-LargeFile": "解析时发生了错误: 目标大小大于 8MB",
73 | "Error-NewRequest": "请求时发生了错误: %s",
74 | "Error-FetchResponse": "获取时发生了错误: %s",
75 | "Error-FetchResponse2": "获取时发生了错误",
76 | "Error-ReadResponse": "读取时发生了错误: %s",
77 | "Error-NoAuth": "请求时发生了错误: 认证失败",
78 | "Error-Forbidden": "请求时发生了错误: 禁止访问",
79 | "Error-NotFound": "请求时发生了错误: 资源不存在",
80 | "Error-UnknownStatusCode": "请求时发生了错误: 未知状态码 %d",
81 | "Error-Parse": "解析时发生了错误: %s",
82 | "Error-Login": "登录时发生了错误",
83 | "Error-FetchUpdate": "获取更新时发生了错误",
84 | "Error-GenJSON": "构造 JSON 时发生了错误: %s",
85 | "Error-GenJSONWithID": "构造 JSON (%s) 时发生了错误: %s",
86 | "Error-Log_Write": "写入日志时发生了错误: %s",
87 | "Error-IPFilter_Write": "写入 IPFilter 时发生了错误: %s",
88 | "Error-LoadLog_Mkdir": "创建日志目录时发生了错误: %s",
89 | "Error-LoadLog_Close": "关闭日志时发生了错误: %s",
90 | "Error-MatchRegexpErr": "正则匹配过程中发生了错误: %s",
91 | "Error-Task_EmptyURL": "检测到 URL 为空, 可能是未配置且未能自动读取客户端配置文件",
92 | "Error-Task_NotSupportClient": "检测到不支持的客户端, 可能是未配置且未能自动检测客户端: %s",
93 | "Error-SyncWithServer_ServerError": "同步服务器错误: %s",
94 | "Error-Debug-EmptyLine": ":%d 为空",
95 | "Error-Debug-EmptyLineWithSource": ":%d 为空 (%s)",
96 | "Error-Debug-GetClientConfigPath_GetUserHomeDir": "获取 User Home 目录时发生了错误: %s",
97 | "Error-Debug-GetClientConfigPath_GetUserConfigDir": "获取 User Config 目录时发生了错误: %s",
98 | "Failed-LoadInitConfig": "读取配置文件失败或不完整",
99 | "Failed-ChangeWorkingDir": "切换工作目录失败: %s",
100 | "Failed-Login_BadUsernameOrPassword": "登录失败: 账号或密码错误",
101 | "Failed-Login_Other": "登录失败: %s",
102 | "Failed-ExecCommand": "执行命令失败, 输出: %s, 错误: %s",
103 | "Success-RegHotkey": "已注册并开始监听窗口热键: CTRL+ALT+B",
104 | "Success-ChangeWorkingDir": "切换工作目录: %s",
105 | "Success-LoadConfig": "加载配置文件 (%s) 成功",
106 | "Success-SetCSRFToken": "设置 CSRF Token 成功: %s",
107 | "Success-SetURL": "读取客户端配置文件成功 (WebUIEnabled: %t, URL: %s, Username: %s)",
108 | "Success-GenIPFilter": "生成了 %d 条 IP 规则",
109 | "Success-SetBlockListFromURL": "本次设置了 %d 条 表达式 规则 (来源: BlockListURL)",
110 | "Success-SetIPBlockListFromURL": "本次设置了 %d 条 IP 规则 (来源: IPBlockListURL)",
111 | "Success-SetBlockListFromFile": "本次设置了 %d 条 表达式 规则 (来源: BlockListFile)",
112 | "Success-SetIPBlockListFromFile": "本次设置了 %d 条 IP 规则 (来源: IPBlockListFile)",
113 | "Success-DetectClient": "检测客户端类型成功: %s",
114 | "Success-Login": "登录成功",
115 | "Success-ClearBlockPeer": "已清理过期客户端: %d 个",
116 | "Success-ExecCommand": "执行命令成功, 输出: %s",
117 | "Success-SyncWithServer": "成功与同步服务器同步",
118 |
119 | // Part ShadowBan.
120 | "Warning-ShadowBanAPINotExist": "未检测到 ShadowBan API, 将使用常规方法",
121 | "Warning-ShadowBanAPIDisabled": "未启用 ShadowBan API, 将使用常规方法",
122 | "Failed-UnknownShadowBanAPI": "检测到未知 Shadow Ban API, 将使用常规方法",
123 | "Failed-GetQBPreferences": "获取 qBittorrent 偏好设置失败",
124 |
125 | }
126 |
127 | func LoadLang(langCode string) bool {
128 | langPath := "lang/" + langCode + ".json"
129 |
130 | _, err := os.Stat(langPath)
131 | if err != nil {
132 | if !os.IsNotExist(err) {
133 | Log("LoadLang", GetLangText("Error-LoadLang"), false, langPath)
134 | }
135 | return false
136 | }
137 |
138 | langFile, err := os.ReadFile(langPath)
139 | if err != nil {
140 | Log("LoadLang", GetLangText("Error-ReadLang"), false, langPath, err.Error())
141 | return false
142 | }
143 |
144 | if err := json.Unmarshal(jsonc.ToJSON(langFile), &langContent); err != nil {
145 | Log("LoadLang", GetLangText("Error-ParseLang"), false, langPath, err.Error())
146 | return false
147 | }
148 |
149 | return true
150 | }
151 | func GetLangCode() string {
152 | langTag, err := locale.Detect()
153 | if err == nil {
154 | return langTag.String()[0:2]
155 | }
156 |
157 | return "en"
158 | }
159 | func GetLangText(uniqueID string) string {
160 | if content, exist := langContent[uniqueID]; exist {
161 | return content
162 | }
163 |
164 | if defaultContent, exist := defaultLangContent[uniqueID]; exist {
165 | return defaultContent
166 | }
167 |
168 | return uniqueID
169 | }
170 |
--------------------------------------------------------------------------------
/client_qBittorrent.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "net/url"
6 | "os"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | type qB_TorrentStruct struct {
12 | InfoHash string `json:"hash"`
13 | NumLeechs int64 `json:"num_leechs"`
14 | TotalSize int64 `json:"total_size"`
15 | Tracker string `json:"tracker"`
16 | }
17 | type qB_PeerStruct struct {
18 | IP string `json:"ip"`
19 | Port int `json:"port"`
20 | Client string `json:"client"`
21 | PeerID string `json:"peer_id_client"`
22 | Progress float64 `json:"progress"`
23 | Downloaded int64 `json:"downloaded"`
24 | Uploaded int64 `json:"uploaded"`
25 | DlSpeed int64 `json:"dl_speed"`
26 | UpSpeed int64 `json:"up_speed"`
27 | }
28 | type qB_TorrentPeersStruct struct {
29 | FullUpdate bool `json:"full_update"`
30 | Peers map[string]qB_PeerStruct `json:"peers"`
31 | }
32 |
33 | var qB_useNewBanPeersMethod = false
34 |
35 | func qB_GetClientConfigPath() string {
36 | var qBConfigFilename string
37 | userHomeDir, err := os.UserHomeDir()
38 | if err != nil {
39 | Log("Debug-GetClientConfigPath", GetLangText("Error-Debug-GetClientConfigPath_GetUserHomeDir"), true, err.Error())
40 | return ""
41 | }
42 | if IsUnix(userHomeDir) {
43 | qBConfigFilename = userHomeDir + "/.config/qBittorrent/qBittorrent.ini"
44 | } else {
45 | userConfigDir, err := os.UserConfigDir()
46 | if err != nil {
47 | Log("Debug-GetClientConfigPath", GetLangText("Error-Debug-GetClientConfigPath_GetUserConfigDir"), true, err.Error())
48 | return ""
49 | }
50 | qBConfigFilename = userConfigDir + "\\qBittorrent\\qBittorrent.ini"
51 | }
52 | return qBConfigFilename
53 | }
54 | func qB_GetClientConfig() []byte {
55 | qBConfigFilename := qB_GetClientConfigPath()
56 | if qBConfigFilename == "" {
57 | return []byte{}
58 | }
59 |
60 | _, err := os.Stat(qBConfigFilename)
61 | if err != nil {
62 | if !os.IsNotExist(err) {
63 | // 避免反复猜测默认 qBittorrent 配置文件的失败信息影响 Debug 用户体验.
64 | Log("GetClientConfig", GetLangText("Error-GetClientConfig_LoadConfigMeta"), true, err.Error())
65 | }
66 | return []byte{}
67 | }
68 |
69 | Log("GetClientConfig", GetLangText("GetClientConfig_UseConfig"), true, qBConfigFilename)
70 |
71 | qBConfigFile, err := os.ReadFile(qBConfigFilename)
72 | if err != nil {
73 | Log("GetClientConfig", GetLangText("Error-GetClientConfig_LoadConfig"), true, err.Error())
74 | return []byte{}
75 | }
76 |
77 | return qBConfigFile
78 | }
79 | func qB_SetURL() bool {
80 | qBConfigFile := qB_GetClientConfig()
81 | if len(qBConfigFile) < 1 {
82 | return false
83 | }
84 | qBConfigFileArr := strings.Split(string(qBConfigFile), "\n")
85 | qBWebUIEnabled := false
86 | qBHTTPSEnabled := false
87 | qBAddress := ""
88 | qBPort := 8080
89 | Username := ""
90 | for _, qbConfigLine := range qBConfigFileArr {
91 | qbConfigLineArr := strings.SplitN(qbConfigLine, "=", 2)
92 | if len(qbConfigLineArr) < 2 || qbConfigLineArr[1] == "" {
93 | continue
94 | }
95 | qbConfigLineArr[0] = strings.ToLower(StrTrim(qbConfigLineArr[0]))
96 | qbConfigLineArr[1] = strings.ToLower(StrTrim(qbConfigLineArr[1]))
97 | switch qbConfigLineArr[0] {
98 | case "webui\\enabled":
99 | if qbConfigLineArr[1] == "true" {
100 | qBWebUIEnabled = true
101 | }
102 | case "webui\\https\\enabled":
103 | if qbConfigLineArr[1] == "true" {
104 | qBHTTPSEnabled = true
105 | }
106 | case "webui\\address":
107 | if qbConfigLineArr[1] == "*" || qbConfigLineArr[1] == "0.0.0.0" {
108 | qBAddress = "127.0.0.1"
109 | } else if qbConfigLineArr[1] == "::" || qbConfigLineArr[1] == "::1" {
110 | qBAddress = "[::1]"
111 | } else {
112 | qBAddress = qbConfigLineArr[1]
113 | }
114 | case "webui\\port":
115 | tmpQBPort, err := strconv.Atoi(qbConfigLineArr[1])
116 | if err == nil {
117 | qBPort = tmpQBPort
118 | }
119 | case "webui\\username":
120 | Username = qbConfigLineArr[1]
121 | }
122 | }
123 | if !qBWebUIEnabled || qBAddress == "" {
124 | Log("SetURL", GetLangText("Abandon-SetURL"), true, qBWebUIEnabled, qBAddress)
125 | return false
126 | }
127 | if qBHTTPSEnabled {
128 | config.ClientURL = "https://" + qBAddress
129 | if qBPort != 443 {
130 | config.ClientURL += ":" + strconv.Itoa(qBPort)
131 | }
132 | } else {
133 | config.ClientURL = "http://" + qBAddress
134 | if qBPort != 80 {
135 | config.ClientURL += ":" + strconv.Itoa(qBPort)
136 | }
137 | }
138 | config.ClientURL += "/api"
139 | config.ClientUsername = Username
140 | Log("SetURL", GetLangText("Success-SetURL"), true, qBWebUIEnabled, config.ClientURL, config.ClientUsername)
141 | return true
142 | }
143 | func qB_GetAPIVersion() bool {
144 | if !strings.HasSuffix(config.ClientURL, "/api") {
145 | apiResponseStatusCodeWithSuffix, _, _ := Fetch(config.ClientURL+"/api/v2/app/webapiVersion", false, false, false, nil)
146 | if apiResponseStatusCodeWithSuffix == 200 || apiResponseStatusCodeWithSuffix == 403 {
147 | config.ClientURL += "/api"
148 | Log("qB_GetAPIVersion", GetLangText("ClientQB_Detect-OldClientURL"), true, config.ClientURL)
149 | return true
150 | }
151 | }
152 |
153 | apiResponseStatusCode, _, _ := Fetch(config.ClientURL+"/v2/app/webapiVersion", false, false, false, nil)
154 | return (apiResponseStatusCode == 200 || apiResponseStatusCode == 403)
155 | }
156 | func qB_Login() bool {
157 | loginParams := url.Values{}
158 | loginParams.Set("username", config.ClientUsername)
159 | loginParams.Set("password", config.ClientPassword)
160 | _, _, loginResponseBody := Submit(config.ClientURL+"/v2/auth/login", loginParams.Encode(), false, true, nil)
161 | if loginResponseBody == nil {
162 | Log("Login", GetLangText("Error-Login"), true)
163 | return false
164 | }
165 |
166 | loginResponseBodyStr := StrTrim(string(loginResponseBody))
167 | if loginResponseBodyStr == "Ok." {
168 | Log("Login", GetLangText("Success-Login"), true)
169 | return true
170 | } else if loginResponseBodyStr == "Fails." {
171 | Log("Login", GetLangText("Failed-Login_BadUsernameOrPassword"), true)
172 | } else {
173 | Log("Login", GetLangText("Failed-Login_Other"), true, loginResponseBodyStr)
174 | }
175 | return false
176 | }
177 | func qB_FetchTorrents() *[]qB_TorrentStruct {
178 | _, _, torrentsResponseBody := Fetch(config.ClientURL+"/v2/torrents/info?filter=active", true, true, false, nil)
179 | if torrentsResponseBody == nil {
180 | Log("FetchTorrents", GetLangText("Error"), true)
181 | return nil
182 | }
183 |
184 | var torrentsResult []qB_TorrentStruct
185 | if err := json.Unmarshal(torrentsResponseBody, &torrentsResult); err != nil {
186 | Log("FetchTorrents", GetLangText("Error-Parse"), true, err.Error())
187 | return nil
188 | }
189 |
190 | return &torrentsResult
191 | }
192 | func qB_FetchTorrentPeers(infoHash string) *qB_TorrentPeersStruct {
193 | _, _, torrentPeersResponseBody := Fetch(config.ClientURL+"/v2/sync/torrentPeers?rid=0&hash="+infoHash, true, true, false, nil)
194 | if torrentPeersResponseBody == nil {
195 | Log("FetchTorrentPeers", GetLangText("Error"), true)
196 | return nil
197 | }
198 |
199 | var torrentPeersResult qB_TorrentPeersStruct
200 | if err := json.Unmarshal(torrentPeersResponseBody, &torrentPeersResult); err != nil {
201 | Log("FetchTorrentPeers", GetLangText("Error-Parse"), true, err.Error())
202 | return nil
203 | }
204 |
205 | /*
206 | if config.Debug_CheckTorrent {
207 | Log("Debug-FetchTorrentPeers", "完整更新: %s", false, strconv.FormatBool(torrentPeersResult.FullUpdate))
208 | }
209 | */
210 |
211 | return &torrentPeersResult
212 | }
213 | func qB_SubmitBlockPeer(blockPeerMap map[string]BlockPeerInfoStruct) bool {
214 | banIPPortsStr := ""
215 |
216 | if blockPeerMap != nil {
217 | if qB_useNewBanPeersMethod {
218 | for peerIP, peerInfo := range blockPeerMap {
219 | if _, exist := peerInfo.Port[-1]; config.BanAllPort || exist {
220 | for port := 0; port <= 65535; port++ {
221 | if IsIPv6(peerIP) {
222 | banIPPortsStr += "[" + peerIP + "]:" + strconv.Itoa(port) + "|"
223 | } else {
224 | banIPPortsStr += peerIP + ":" + strconv.Itoa(port) + "|"
225 | banIPPortsStr += "[::ffff:" + peerIP + "]:" + strconv.Itoa(port) + "|"
226 | }
227 | }
228 | continue
229 | }
230 | for port, _ := range peerInfo.Port {
231 | banIPPortsStr += peerIP + ":" + strconv.Itoa(port) + "|"
232 | }
233 | }
234 | banIPPortsStr = strings.TrimRight(banIPPortsStr, "|")
235 | } else {
236 | for peerIP := range blockPeerMap {
237 | banIPPortsStr += peerIP + "\n"
238 | if !IsIPv6(peerIP) {
239 | banIPPortsStr += "::ffff:" + peerIP + "\n"
240 | }
241 | }
242 | }
243 | }
244 |
245 | Log("Debug-SubmitBlockPeer", "%s", false, banIPPortsStr)
246 |
247 | var banResponseBody []byte
248 |
249 | if qB_useNewBanPeersMethod && banIPPortsStr != "" {
250 | banIPPortsStr = url.QueryEscape(banIPPortsStr)
251 | _, _, banResponseBody = Submit(config.ClientURL+"/v2/transfer/banPeers", banIPPortsStr, true, true, nil)
252 | } else {
253 | banIPPortsStr = url.QueryEscape("{\"banned_IPs\": \"" + banIPPortsStr + "\"}")
254 | _, _, banResponseBody = Submit(config.ClientURL+"/v2/app/setPreferences", "json="+banIPPortsStr, true, true, nil)
255 | }
256 |
257 | if banResponseBody == nil {
258 | Log("SubmitBlockPeer", GetLangText("Error"), true)
259 | return false
260 | }
261 |
262 | return true
263 | }
264 |
265 | func qB_GetPreferences() map[string]interface{} {
266 | _, _, responseBody := Submit(config.ClientURL+"/v2/app/preferences", "", true, true, nil)
267 | if responseBody == nil {
268 | Log("GetPreferences", GetLangText("Failed-GetQBPreferences"), true)
269 | return nil
270 | }
271 |
272 | var preferences map[string]interface{}
273 | if err := json.Unmarshal(responseBody, &preferences); err != nil {
274 | Log("GetPreferences", GetLangText("Error-Parse"), true, err.Error())
275 | return nil
276 | }
277 |
278 | return preferences
279 | }
280 | func qB_TestShadowBanAPI() bool {
281 | // 1. Check if enable_shadowban is true;
282 | // enable_shadowban may be not exist in the preferences.
283 | pref := qB_GetPreferences()
284 | if pref == nil {
285 | return false
286 | }
287 |
288 | enableShadowBan, exist := pref["shadow_ban_enabled"]
289 | if !exist {
290 | Log("TestShadowBanAPI", GetLangText("Warning-ShadowBanAPINotExist"), true)
291 | return false
292 | }
293 |
294 | if bEnableShadowBan, ok := enableShadowBan.(bool); ok {
295 | if !bEnableShadowBan {
296 | return false
297 | }
298 | } else {
299 | Log("TestShadowBanAPI", GetLangText("Failed-UnknownShadowBanAPI"), true)
300 | return false
301 | }
302 |
303 | // 2. Check if the API is available;
304 | code, _, _ := Submit(config.ClientURL+"/v2/transfer/shadowbanPeers", "peers=", true, true, nil)
305 | if code != 200 {
306 | Log("TestShadowBanAPI", GetLangText("Warning-ShadowBanAPINotExist"), true)
307 | return false
308 | }
309 |
310 | return true
311 | }
312 | func qB_SubmitShadowBanPeer(blockPeerMap map[string]BlockPeerInfoStruct) bool {
313 | shadowBanIPPortsList := []string{}
314 | for peerIP, peerInfo := range blockPeerMap {
315 | for port := range peerInfo.Port {
316 | if port <= 0 || port > 65535 {
317 | port = 1 // Seems qBittorrent will ignore the invalid port number, so we just set it to 1.
318 | }
319 | if IsIPv6(peerIP) {
320 | shadowBanIPPortsList = append(shadowBanIPPortsList, "[" + peerIP + "]:" + strconv.Itoa(port))
321 | } else {
322 | shadowBanIPPortsList = append(shadowBanIPPortsList, peerIP + ":" + strconv.Itoa(port))
323 | shadowBanIPPortsList = append(shadowBanIPPortsList, "[::ffff:" + peerIP + "]:" + strconv.Itoa(port))
324 | }
325 | }
326 | }
327 |
328 | banIPPortsStr := strings.Join(shadowBanIPPortsList, "|")
329 | Log("Debug-SubmitShadowBanPeer", "%s", false, banIPPortsStr)
330 |
331 | var banResponseBody []byte
332 |
333 | if banIPPortsStr != "" {
334 | banIPPortsStr = url.QueryEscape(banIPPortsStr)
335 | _, _, banResponseBody = Submit(config.ClientURL+"/v2/transfer/shadowbanPeers", "peers="+banIPPortsStr, true, true, nil)
336 | } else {
337 | return true
338 | }
339 |
340 | if banResponseBody == nil {
341 | Log("SubmitShadowBanPeer", GetLangText("Error"), true)
342 | return false
343 | }
344 |
345 | return true
346 | }
347 |
--------------------------------------------------------------------------------
/peer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/dlclark/regexp2"
9 | )
10 |
11 | type BlockPeerInfoStruct struct {
12 | Timestamp int64
13 | Module string
14 | Reason string
15 | Port map[int]bool
16 | InfoHash string
17 | }
18 | type BlockCIDRInfoStruct struct {
19 | Timestamp int64
20 | Net *net.IPNet
21 | IPs map[string]bool
22 | }
23 |
24 | var lastCleanTimestamp int64 = 0
25 | var blockPeerMap = make(map[string]BlockPeerInfoStruct)
26 | var blockCIDRMap = make(map[string]BlockCIDRInfoStruct)
27 |
28 | func AddBlockPeer(module string, reason string, peerIP string, peerPort int, torrentInfoHash string) {
29 | var blockPeerPortMap map[int]bool
30 | if blockPeer, exist := blockPeerMap[peerIP]; !exist {
31 | blockPeerPortMap = make(map[int]bool)
32 | } else {
33 | blockPeerPortMap = blockPeer.Port
34 | }
35 |
36 | blockPeerPortMap[peerPort] = true
37 | blockPeerMap[peerIP] = BlockPeerInfoStruct{Timestamp: currentTimestamp, Module: module, Reason: reason, Port: blockPeerPortMap, InfoHash: torrentInfoHash}
38 |
39 | AddBlockCIDR(peerIP, ParseIPCIDRByConfig(peerIP))
40 |
41 | if config.ExecCommand_Ban != "" {
42 | execCommand_Ban := config.ExecCommand_Ban
43 | execCommand_Ban = strings.Replace(execCommand_Ban, "{peerIP}", peerIP, -1)
44 | execCommand_Ban = strings.Replace(execCommand_Ban, "{peerPort}", strconv.Itoa(peerPort), -1)
45 | execCommand_Ban = strings.Replace(execCommand_Ban, "{torrentInfoHash}", torrentInfoHash, -1)
46 | status, out, err := ExecCommand(execCommand_Ban)
47 |
48 | if status {
49 | Log("AddBlockPeer", GetLangText("Success-ExecCommand"), true, out)
50 | } else {
51 | Log("AddBlockPeer", GetLangText("Failed-ExecCommand"), true, out, err)
52 | }
53 | }
54 | }
55 | func AddBlockCIDR(peerIP string, peerNet *net.IPNet) {
56 | if peerNet == nil {
57 | return
58 | }
59 |
60 | peerNetStr := peerNet.String()
61 | var blockIPsMap map[string]bool
62 | if blockCIDRInfo, exist := blockCIDRMap[peerNetStr]; !exist {
63 | blockIPsMap = make(map[string]bool)
64 | } else {
65 | blockIPsMap = blockCIDRMap[peerNetStr].IPs
66 | if _, exist := blockCIDRInfo.IPs[peerIP]; !exist {
67 | blockIPsMap[peerIP] = true
68 | }
69 | }
70 |
71 | blockCIDRMap[peerNetStr] = BlockCIDRInfoStruct{Timestamp: currentTimestamp, Net: peerNet, IPs: blockIPsMap}
72 | }
73 | func ClearBlockPeer() int {
74 | cleanCount := 0
75 | if blockPeerMap != nil && config.CleanInterval == 0 || (lastCleanTimestamp+int64(config.CleanInterval) < currentTimestamp) {
76 | for peerIP, peerInfo := range blockPeerMap {
77 | if currentTimestamp > (peerInfo.Timestamp + int64(config.BanTime)) {
78 | cleanCount++
79 | delete(blockPeerMap, peerIP)
80 |
81 | peerNet := ParseIPCIDRByConfig(peerIP)
82 |
83 | if peerNet != nil {
84 | peerNetStr := peerNet.String()
85 | if blockCIDRInfo, exist := blockCIDRMap[peerNetStr]; exist {
86 | if blockCIDRInfo.Timestamp > peerInfo.Timestamp {
87 | peerInfo.Timestamp = blockCIDRInfo.Timestamp
88 | blockPeerMap[peerIP] = peerInfo
89 | continue
90 | }
91 |
92 | delete(blockCIDRInfo.IPs, peerIP)
93 | if len(blockCIDRInfo.IPs) <= 0 {
94 | delete(blockCIDRMap, peerNetStr)
95 | continue
96 | }
97 |
98 | blockCIDRMap[peerNetStr] = blockCIDRInfo
99 | }
100 | }
101 |
102 | if config.ExecCommand_Unban != "" {
103 | for peerPort, _ := range peerInfo.Port {
104 | execCommand_Unban := config.ExecCommand_Unban
105 | execCommand_Unban = strings.Replace(execCommand_Unban, "{peerIP}", peerIP, -1)
106 | execCommand_Unban = strings.Replace(execCommand_Unban, "{peerPort}", strconv.Itoa(peerPort), -1)
107 | execCommand_Unban = strings.Replace(execCommand_Unban, "{torrentInfoHash}", peerInfo.InfoHash, -1)
108 | status, out, err := ExecCommand(execCommand_Unban)
109 |
110 | if status {
111 | Log("AddBlockPeer", GetLangText("Success-ExecCommand"), true, out)
112 | } else {
113 | Log("AddBlockPeer", GetLangText("Failed-ExecCommand"), true, out, err)
114 | }
115 | }
116 | }
117 | }
118 | }
119 | if cleanCount != 0 {
120 | lastCleanTimestamp = currentTimestamp
121 | Log("ClearBlockPeer", GetLangText("Success-ClearBlockPeer"), true, cleanCount)
122 | }
123 | }
124 |
125 | return cleanCount
126 | }
127 | func IsBlockedPeer(peerIP string, peerPort int, updateTimestamp bool) bool {
128 | if blockPeer, exist := blockPeerMap[peerIP]; exist {
129 | if IsBanPort() {
130 | if _, exist1 := blockPeer.Port[-1]; !exist1 {
131 | if _, exist2 := blockPeer.Port[peerPort]; !exist2 {
132 | return false
133 | }
134 | }
135 | }
136 |
137 | if updateTimestamp {
138 | blockPeer.Timestamp = currentTimestamp
139 | blockPeerMap[peerIP] = blockPeer
140 | }
141 |
142 | return true
143 | }
144 |
145 | return false
146 | }
147 | func MatchBlockList(blockRegex *regexp2.Regexp, peerIP string, peerPort int, peerID string, peerClient string) bool {
148 | if blockRegex != nil {
149 | if peerClient != "" {
150 | isMatchPeerClient, err := blockRegex.MatchString(peerClient)
151 |
152 | if err != nil {
153 | Log("MatchBlockList_PeerClient", GetLangText("Error-MatchRegexpErr"), true, err.Error())
154 | } else if isMatchPeerClient {
155 | return true
156 | }
157 | }
158 |
159 | if peerID != "" {
160 | isMatchPeerID, err := blockRegex.MatchString(peerID)
161 |
162 | if err != nil {
163 | Log("MatchBlockList_PeerID", GetLangText("Error-MatchRegexpErr"), true, err.Error())
164 | } else if isMatchPeerID {
165 | return true
166 | }
167 | }
168 | }
169 |
170 | return false
171 | }
172 | func CheckPeer(peerIP string, peerPort int, peerID, peerClient string, peerDlSpeed, peerUpSpeed int64, peerProgress float64, peerDownloaded, peerUploaded int64, torrentInfoHash string, torrentTotalSize int64) (int, *net.IPNet) {
173 | if peerIP == "" || CheckPrivateIP(peerIP) {
174 | return -1, nil
175 | }
176 |
177 | if IsBlockedPeer(peerIP, peerPort, true) {
178 | Log("Debug-CheckPeer_IgnorePeer (Blocked)", "%s:%d %s|%s", false, peerIP, peerPort, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient))
179 | /*
180 | if peerPort == -2 {
181 | return 4
182 | }
183 | */
184 | if peerPort == -1 {
185 | return 3, nil
186 | }
187 | return 2, nil
188 | }
189 |
190 | peerNet := ParseIPCIDRByConfig(peerIP)
191 | hasPeerClient := (peerID != "" || peerClient != "")
192 |
193 | if hasPeerClient {
194 | earlyStop := false
195 | blockListCompiled.Range(func(key, val any) bool {
196 | if MatchBlockList(val.(*regexp2.Regexp), peerIP, peerPort, peerID, peerClient) {
197 | Log("CheckPeer_AddBlockPeer (Bad-Client_Normal)", "%s:%d %s|%s (TorrentInfoHash: %s)", true, peerIP, peerPort, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash)
198 | AddBlockPeer("CheckPeer", "Bad-Client_Normal", peerIP, peerPort, torrentInfoHash)
199 | earlyStop = true
200 | return false
201 | }
202 | return true
203 | })
204 |
205 | if earlyStop {
206 | return 1, peerNet
207 | }
208 | }
209 |
210 | for port := range config.PortBlockList {
211 | if port == peerPort {
212 | Log("CheckPeer_AddBlockPeer (Bad-Port)", "%s:%d %s|%s (TorrentInfoHash: %s)", true, peerIP, peerPort, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash)
213 | AddBlockPeer("CheckPeer", "Bad-Port", peerIP, peerPort, torrentInfoHash)
214 | return 1, peerNet
215 | }
216 | }
217 |
218 | ip := net.ParseIP(peerIP)
219 | if ip == nil {
220 | Log("Debug-CheckPeer_AddBlockPeer (Bad-IP)", "%s:%d %s|%s (TorrentInfoHash: %s)", false, peerIP, -1, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash)
221 | } else {
222 | earlyStop := false
223 | ipBlockListCompiled.Range(func(_, v any) bool {
224 | if v == nil {
225 | return true
226 | }
227 |
228 | ipNet, ok := (v).(*net.IPNet)
229 | if !ok {
230 | return true
231 | }
232 | if ipNet.Contains(ip) {
233 | Log("CheckPeer_AddBlockPeer (Bad-IP_Normal)", "%s:%d %s|%s (TorrentInfoHash: %s)", true, peerIP, -1, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash)
234 | AddBlockPeer("CheckPeer", "Bad-IP_Normal", peerIP, -1, torrentInfoHash)
235 | earlyStop = true
236 | return false
237 | }
238 |
239 | return true
240 | })
241 | if earlyStop {
242 | return 3, peerNet
243 | }
244 |
245 | for _, v := range ipBlockCIDRMapFromSyncServerCompiled {
246 | if v == nil {
247 | continue
248 | }
249 | if v.Contains(ip) {
250 | Log("CheckPeer_AddBlockPeer (Bad-IP_FromSyncServer)", "%s:%d %s|%s (TorrentInfoHash: %s)", true, peerIP, -1, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash)
251 | AddBlockPeer("CheckPeer", "Bad-IP_FromSyncServer", peerIP, -1, torrentInfoHash)
252 | return 3, peerNet
253 | }
254 | }
255 | }
256 |
257 | if IsMatchCIDR(peerNet) {
258 | Log("CheckPeer_AddBlockPeer (Bad-CIDR)", "%s:%d %s|%s (TorrentInfoHash: %s, PeerNet: %s)", true, peerIP, peerPort, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash, peerNet.String())
259 | AddBlockPeer("CheckPeer", "Bad-CIDR", peerIP, peerPort, torrentInfoHash)
260 | return 1, peerNet
261 | }
262 |
263 | if peerDlSpeed <= 0 && peerUpSpeed <= 0 {
264 | return -2, peerNet
265 | }
266 |
267 | ignoreByDownloaded := false
268 | // 若启用忽略且遇到空信息 Peer, 则既不会启用绝对进度屏蔽, 也不会记录 IP 及 Torrent 信息.
269 | if !config.IgnoreEmptyPeer || hasPeerClient {
270 | if config.IgnoreByDownloaded > 0 && (peerDownloaded/1024/1024) >= int64(config.IgnoreByDownloaded) {
271 | ignoreByDownloaded = true
272 | }
273 | if !ignoreByDownloaded && IsProgressNotMatchUploaded(torrentTotalSize, peerProgress, peerUploaded) {
274 | Log("CheckPeer_AddBlockPeer (Bad-Progress_Uploaded)", "%s:%d %s|%s (TorrentInfoHash: %s, TorrentTotalSize: %.2f MB, PeerDlSpeed: %.2f MB/s, PeerUpSpeed: %.2f MB/s, Progress: %.2f%%, Downloaded: %.2f MB, Uploaded: %.2f MB)", true, peerIP, peerPort, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash, (float64(torrentTotalSize) / 1024 / 1024), (float64(peerDlSpeed) / 1024 / 1024), (float64(peerUpSpeed) / 1024 / 1024), (peerProgress * 100), (float64(peerDownloaded) / 1024 / 1024), (float64(peerUploaded) / 1024 / 1024))
275 | AddBlockPeer("CheckPeer", "Bad-Progress_Uploaded", peerIP, peerPort, torrentInfoHash)
276 | return 1, peerNet
277 | }
278 | }
279 |
280 | if (config.IgnoreEmptyPeer && !hasPeerClient) || ignoreByDownloaded {
281 | return -2, peerNet
282 | }
283 |
284 | return 0, peerNet
285 | }
286 | func ProcessPeer(peerIP string, peerPort int, peerID string, peerClient string, peerDlSpeed int64, peerUpSpeed int64, peerProgress float64, peerDownloaded int64, peerUploaded int64, torrentInfoHash string, torrentTotalSize int64, blockCount *int, ipBlockCount *int, badPeersCount *int, emptyPeersCount *int) {
287 | peerIP = ProcessIP(peerIP)
288 | peerStatus, peerNet := CheckPeer(peerIP, peerPort, peerID, peerClient, peerDlSpeed, peerUpSpeed, peerProgress, peerDownloaded, peerUploaded, torrentInfoHash, torrentTotalSize)
289 | if config.Debug_CheckPeer {
290 | Log("Debug-CheckPeer", "%s:%d %s|%s (TorrentInfoHash: %s, TorrentTotalSize: %.2f MB, PeerDlSpeed: %.2f MB/s, PeerUpSpeed: %.2f MB/s, Progress: %.2f%%, Downloaded: %.2f MB, Uploaded: %.2f MB, PeerStatus: %d)", false, peerIP, peerPort, strconv.QuoteToASCII(peerID), strconv.QuoteToASCII(peerClient), torrentInfoHash, (float64(torrentTotalSize) / 1024 / 1024), (float64(peerDlSpeed) / 1024 / 1024), (float64(peerUpSpeed) / 1024 / 1024), (peerProgress * 100), (float64(peerDownloaded) / 1024 / 1024), (float64(peerUploaded) / 1024 / 1024), peerStatus)
291 | }
292 |
293 | switch peerStatus {
294 | case 1:
295 | *blockCount++
296 | case 3:
297 | *ipBlockCount++
298 | case -1:
299 | *badPeersCount++
300 | case -2:
301 | *emptyPeersCount++
302 | case 0:
303 | if peerNet == nil {
304 | AddIPInfo(nil, peerIP, peerPort, torrentInfoHash, peerUploaded)
305 | AddTorrentInfo(torrentInfoHash, torrentTotalSize, nil, peerIP, peerPort, peerProgress, peerUploaded)
306 | } else {
307 | AddIPInfo(peerNet, peerNet.String(), peerPort, torrentInfoHash, peerUploaded)
308 | AddTorrentInfo(torrentInfoHash, torrentTotalSize, peerNet, peerNet.String(), peerPort, peerProgress, peerUploaded)
309 | }
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qBittorrent-ClientBlocker
2 |
3 | [中文 (默认, Beta 版本)](README.md) [English (Default, Beta Version)](README.en.md)
4 | [中文 (Public 正式版)](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/blob/master/README.md) [English (Public version)](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/blob/master/README.en.md)
5 |
6 | 一款适用于 qBittorrent (4.1+)/Transmission (3.0+, Beta)/BitComet (2.0+, Beta, Partial) 的客户端屏蔽器, 默认屏蔽包括但不限于迅雷等客户端.
7 |
8 | - 全平台支持
9 | - 支持记录日志及热重载配置
10 | - 支持忽略私有 IP 地址
11 | - 支持自定义屏蔽列表 (不区分大小写, 支持正则表达式)
12 | - 支持多种客户端及其认证, 同时可自动检测部分客户端 (目前支持 qBittorrent. 可以安全忽略检测客户端时发生的错误)
13 | - 支持增强自动屏蔽 (默认禁用): 根据默认或设定的相关参数自动屏蔽 Peer
14 | - 在 Windows 下支持通过系统托盘或窗口热键 (CTRL+ALT+B) 显示及隐藏窗口 (部分用户[反馈](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/issues/10)其可能会影响屏蔽, 由于原因不明, 若遇到相关问题可避免使用该功能)
15 |
16 | 通常可以忽略偶发错误, 其原因可能有: 1. 网络问题; 2. 客户端超时; 3. Transmission 定期轮换 CSRF Token; ...
17 |
18 | 
19 |
20 | ## 使用 Usage
21 |
22 | ### 前提
23 |
24 | - 必须启用客户端的 Web UI 功能.
25 |
26 | ### 常规版本使用
27 |
28 | 1. 从 [**GitHub Release**](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/releases) 下载压缩包并解压;
29 |
30 |
31 | 查看 常见平台下载版本 对照表
32 |
33 | | 操作系统 | 处理器架构 | 处理器位数 | 下载版本 | 说明 |
34 | | -------- | ---------- | ---------- | ------------- | ----------------- |
35 | | macOS | ARM64 | 64 位 | darwin-arm64 | 常见于 Apple M 系列 |
36 | | macOS | AMD64 | 64 位 | darwin-amd64 | 常见于 Intel 系列 |
37 | | Windows | AMD64 | 64 位 | windows-amd64 | 常见于大部分现代 PC |
38 | | Windows | i386 | 32 位 | windows-386 | 少见于部分老式 PC |
39 | | Windows | ARM64 | 64 位 | windows-arm64 | 常见于新型平台, 应用于部分平板/笔记本/少数特殊硬件 |
40 | | Windows | ARMv7 | 32 位 | windows-arm | 少见于罕见平台, 应用于部分上古硬件, 如 Surface RT 等 |
41 | | Linux | AMD64 | 64 位 | linux-amd64 | 常见于大部分 NAS 及服务器 |
42 | | Linux | i386 | 32 位 | linux-386 | 少见于部分老式 NAS 及服务器 |
43 | | Linux | ARM64 | 64 位 | linux-arm64 | 常见于部分服务器及开发板, 如 Oracle 或 Raspberry Pi 等 |
44 | | Linux | ARMv* | 32 位 | linux-armv* | 少见于部分老式服务器及开发板, 查看 /proc/cpuinfo 或 从高到底试哪个能跑 |
45 |
46 | 其它版本的 Linux/NetBSD/FreeBSD/OpenBSD/Solaris 可以此类推, 并在列表中选择适合自己的.
47 |
48 |
49 | 2. 解压后, 可能需要修改随附的配置文件 ```config.json```;
50 |
51 | - 可根据高级需求, 按需设置配置, 具体见 [配置 Config](#配置-config).
52 | - 若客户端屏蔽器运行于本机, 但未启用客户端 "跳过本机客户端认证" (及密码不为空且并未手动设置密码), 则必须修改配置文件, 并填写 ```clientPassword```.
53 | - 若客户端屏蔽器不运行于本机 或 客户端未安装在默认路径 或 客户端不支持自动读取配置文件, 则必须修改配置文件, 并填写 ```clientURL```/```clientUsername```/```clientPassword```.
54 |
55 | 3. 启动客户端屏蔽器, 并观察信息输出是否正常即可;
56 |
57 | 对于 Windows, 可选修改客户端的快捷方式, 放入自己的屏蔽器路径, 使客户端与屏蔽器同时运行;
58 |
59 | qBittorrent: ```C:\Windows\System32\cmd.exe /c "(tasklist | findstr qBittorrent-ClientBlocker || start C:\Users\Example\qBittorrent-ClientBlocker\qBittorrent-ClientBlocker.exe) && start qbittorrent.exe"```
60 |
61 | 对于 macOS, 可选使用一基本 [LaunchAgent 用户代理](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/wiki#launchagent-macos) 用于开机自启及后台运行;
62 |
63 | 对于 Linux, 可选使用一基本 [Systemd 服务配置文件](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/wiki#systemd-linux) 用于开机自启及后台运行;
64 |
65 | ### Docker 版本使用
66 |
67 | - 从 [**Docker Hub**](https://hub.docker.com/r/simpletracker/qbittorrent-clientblocker) 拉取 Docker 镜像.
68 |
69 | ```
70 | docker pull simpletracker/qbittorrent-clientblocker:latest
71 | ```
72 |
73 | - 配置方法一: 使用 配置文件映射
74 |
75 | 1. 在合适位置新建 ```config.json``` 作为配置文件, 具体内容可参考 [config.json](config.json) 及 [配置 Config](#配置-config);
76 |
77 | 2. 填入 ```clientURL```/```clientUsername```/```clientPassword```;
78 |
79 | - 可根据高级需求, 按需设置其它配置, 具体见 [配置 Config](#配置-config).
80 | - 若启用客户端的 "IP 子网白名单", 则可不填写 ```clientUsername``` 和 ```clientPassword```.
81 |
82 | 3. 运行 Docker 并查看日志, 观察信息输出是否正常即可;
83 |
84 | 以下命令模版仅作为参考, 请替换 ```/path/config.json``` 为你的配置文件路径 (挂载前应先创建文件).
85 |
86 | ```
87 | docker run -d \
88 | --name=qbittorrent-clientblocker --network=bridge --restart unless-stopped \
89 | -v /path/config.json:/app/config.json \
90 | simpletracker/qbittorrent-clientblocker:latest
91 | ```
92 |
93 | - 配置方法二: 使用 环境变量
94 |
95 | - 使用前提: 设置 ```useENV``` 环境变量为 ```true```.
96 | - 使用环境变量按需配置设置, 具体见 [配置 Config](#配置-config).
97 | - 若设置较复杂, 则可能出现 blockList 不生效的情况. 因此, 若需要配置此设置, 则使用环境变量是不推荐的.
98 | - 以下命令模版仅作为参考.
99 |
100 | ```
101 | docker run -d \
102 | --name=qbittorrent-clientblocker --network=bridge --restart unless-stopped \
103 | -e useENV=true \
104 | -e debug=false \
105 | -e logPath=logs \
106 | -e blockList='["ExampleBlockList1", "ExampleBlockList2"]' \
107 | -e clientURL=http://example.com \
108 | -e clientUsername=exampleUser \
109 | -e clientPassword=examplePass \
110 | simpletracker/qbittorrent-clientblocker:latest
111 | ```
112 |
113 | ## 参数 Flag
114 |
115 | | 设置项 | 默认值 | 配置说明 |
116 | | ----- | ----- | ----- |
117 | | -v/--version | false (禁用) | 显示程序版本后退出 |
118 | | -c/--config | config.json | 配置文件路径 |
119 | | -ca/--config_additional | config_additional.json | 附加配置文件路径 |
120 | | --debug | false (禁用) | 调试模式. 加载配置文件前生效 |
121 | | --startdelay | 0 (秒, 禁用) | 启动延迟. 部分用户的特殊用途 |
122 | | --nochdir | false (禁用) | 不切换工作目录. 默认会切换至程序目录 |
123 | | --reghotkey | true (启用) | 注册窗口热键. 仅 Windows 可用. 窗口热键固定为 CTRL+ALT+B |
124 | | --hidewindow | false (禁用) | 默认隐藏窗口. 仅 Windows 可用 |
125 | | --hidesystray | false (禁用) | 默认隐藏托盘图标. 仅 Windows 可用 |
126 |
127 | ## 配置 Config
128 |
129 | Docker 版本通过相同名称的环境变量配置, 通过自动转换环境变量为配置文件实现.
130 |
131 | | 设置项 | 类型 | 默认值 | 配置说明 |
132 | | ----- | ----- | ----- | ----- |
133 | | checkUpdate | bool | true (启用) | 检查更新. 默认会自动检查更新 |
134 | | debug | bool | false (禁用) | 调试模式. 启用可看到更多信息, 但可能扰乱视野 |
135 | | debug_CheckTorrent | string | false (禁用) | 调试模式 (CheckTorrent, 须先启用 debug). 启用后调试信息会包括每个 Torrent Hash, 但信息量较大 |
136 | | debug_CheckPeer | string | false (禁用) | 调试模式 (CheckPeer, 须先启用 debug). 启用后调试信息会包括每个 Torrent Peer, 但信息量较大 |
137 | | interval | uint32 | 6 (秒) | 屏蔽循环间隔 (不支持热重载). 每个循环间隔会从后端获取相关信息用于判断及屏蔽, 短间隔有助于降低封禁耗时但可能造成客户端卡顿, 长间隔有助于降低 CPU 资源占用 |
138 | | cleanInterval | uint32 | 3600 (秒) | 屏蔽清理间隔. 短间隔会使过期 Peer 在达到屏蔽持续时间后更快被解除屏蔽, 长间隔有助于合并清理过期 Peer 日志 |
139 | | updateInterval | uint32 | 86400 (秒) | 列表 URL 更新间隔 (blockListURL/ipBlockListURL). 合理的间隔有助于提高更新效率并降低网络占用 |
140 | | restartInterval | uint32 | 6 (秒) | 重启 Torrent 间隔. 用于部分客户端 (Transmission) 屏蔽列表无法立即生效的措施, 通过重启 Torrent 来实现. 过短间隔可能造成屏蔽不生效 |
141 | | torrentMapCleanInterval | uint32 | 60 (秒) | Torrent Map 清理间隔 (启用 ipUploadedCheck+ipUpCheckPerTorrentRatio/banByRelativeProgressUploaded 后生效, 也是其判断间隔). 短间隔可使判断更频繁但可能造成滞后误判 |
142 | | banTime | uint32 | 86400 (秒) | 屏蔽持续时间. 短间隔会使 Peer 更快被解除屏蔽 |
143 | | banAllPort | bool | true (启用) | 屏蔽 IP 所有端口. 默认启用且当前不支持设置 |
144 | | banIPCIDR | string | /32 | 封禁 IPv4 CIDR. 可扩大单个 Peer 的封禁 IP 范围 |
145 | | banIP6CIDR | string | /128 | 封禁 IPv6 CIDR. 可扩大单个 Peer 的封禁 IP 范围 |
146 | | ignoreEmptyPeer | bool | true (启用) | 忽略无 PeerID 及 ClientName 的 Peer. 通常出现于连接未完全建立的客户端 |
147 | | ignoreNoLeechersTorrent | bool | false (禁用) | 忽略没有下载者的 Torrent. 启用后有助于提高性能, 但部分客户端 (如 qBittorrent) 可能会出现不准确的问题 |
148 | | ignorePTTorrent | bool | true (启用) | 忽略 PT Torrent. 若主要 Tracker 包含 ```?passkey=```/```?authkey=```/```?secure=```/```32 位大小写英文及数字组成的字符串``` |
149 | | ignoreFailureExit | bool | false (禁用) | 忽略失败退出. 启用后会使得首次检测客户端失败或认证失败后继续重试 |
150 | | sleepTime | uint32 | 20 (毫秒) | 查询每个 Torrent Peers 的等待时间. 短间隔可使屏蔽 Peer 更快但可能造成客户端卡顿, 长间隔有助于平均 CPU 资源占用 |
151 | | timeout | uint32 | 6 (秒) | 请求超时. 过短间隔可能会造成无法正确屏蔽 Peer, 过长间隔会使超时请求影响屏蔽其它 Peer 的性能 |
152 | | proxy | string | Auto (自动) | 使用代理. 仍会在首次加载配置文件时自动检测代理. 空: 禁止使用代理; Auto: 自动 (仅对外部请求使用代理); All: 对全部请求使用代理 |
153 | | longConnection | bool | true (启用) | 长连接. 启用可降低资源消耗 |
154 | | logPath | string | logs | 日志目录. 须先启用 logToFile |
155 | | logToFile | bool | true (启用) | 记录普通信息到日志. 启用后可用于一般的分析及统计用途 |
156 | | logDebug | bool | false (禁用) | 记录调试信息到日志 (须先启用 debug 及 logToFile). 启用后可用于进阶的分析及统计用途, 但信息量较大 |
157 | | listen | string | 127.0.0.1:26262 | 监听端口. 用于向部分客户端 (Transmission) 提供 BlockPeerList. 非本机使用可改为 ```:``` |
158 | | clientType | string | 空 | 客户端类型. 使用客户端屏蔽器的前提条件, 若未能自动检测客户端类型, 则须正确填入. 目前支持 ```qBittorrent```/```Transmission```/```BitComet``` |
159 | | clientURL | string | 空 | 客户端地址. 可用 Web API 或 RPC 地址. 使用客户端屏蔽器的前提条件, 若未能自动读取客户端配置文件, 则须正确填入. 前缀必须指定 http 或 https 协议, 如 ```http://127.0.0.1:990/api``` 或 ```http://127.0.0.1:9091/transmission/rpc``` |
160 | | clientUsername | string | 空 | 客户端账号. 留空会跳过认证. 若启用客户端内 "跳过本机客户端认证" 可默认留空, 因可自动读取客户端配置文件并设置 |
161 | | clientPassword | string | 空 | 客户端密码. 若启用客户端内 "跳过本机客户端认证" 可默认留空 |
162 | | useBasicAuth | bool | false (禁用) | 同时通过 HTTP Basic Auth 进行认证. 适合只支持 Basic Auth (如 Transmission/BitComet) 或通过反向代理等方式 增加/换用 认证方式的后端 |
163 | | useShadowBan | bool | true (启用) | 使用 ShadowBan API 进行封禁. 仅适用于支持 ShadowBan API 的客户端 (如 qBEE), 若载入配置时检测相关 API 不可用, 将自动禁用 |
164 | | skipCertVerification | bool | false (禁用) | 跳过 Web API 证书校验. 适合自签及过期证书 |
165 | | fetchFailedThreshold | int | 0 (禁用) | 最大获取失败次数. 当超过设定次数, 将执行设置的外部命令 |
166 | | execCommand_FetchFailed | string | 空 | 执行外部命令 (FetchFailed). 首个参数被视作外部程序路径, 当获取失败次数超过设定次数后执行 |
167 | | execCommand_Run | string | 空 | 执行外部命令 (Run). 首个参数被视作外部程序路径, 当程序启动后执行 |
168 | | execCommand_Ban | string | 空 | 执行外部命令 (Ban). 首个参数被视作外部程序路径, 各参数均应使用 ```\|``` 分割, 命令可以使用 ```{peerIP}```/```{peerPort}```/```{torrentInfoHash}``` 来使用相关信息 (peerPort=-1 意味着全端口封禁) |
169 | | execCommand_Unban | string | 空 | 执行外部命令 (Unban). 首个参数被视作外部程序路径, 各参数均应使用 ```\|``` 分割, 命令可以使用 ```{peerIP}```/```{peerPort}```/```{torrentInfoHash}``` 来使用相关信息 (peerPort=-1 意味着全端口封禁) |
170 | | syncServerURL | string | 空 | 同步服务器 URL. 同步服务器会将 TorrentMap 提交至服务器, 并从服务器接收屏蔽 IPCIDR 列表 |
171 | | syncServerToken | string | 空 | 同步服务器 Token. 部分同步服务器可能需要认证 |
172 | | blockList | []string | 空 (于 config.json 附带) | 屏蔽客户端列表. 同时判断 PeerID 及 ClientName, 不区分大小写, 支持正则表达式 |
173 | | blockListURL | []string | 空 | 屏蔽客户端列表 URL. 支持格式同 blockList, 一行一条 |
174 | | blockListFile | []string | 空 | 屏蔽客户端列表文件. 支持格式同 blockList, 一行一条 |
175 | | portBlockList | []uint32 | 空 | 屏蔽端口列表. 若 Peer 端口与列表内任意端口匹配, 则允许屏蔽 Peer |
176 | | ipBlockList | []string | 空 | 屏蔽 IP 列表. 支持不包括端口的 IP (1.2.3.4) 及 IPCIDR (2.3.3.3/3) |
177 | | ipBlockListURL | []string | 空 | 屏蔽 IP 列表 URL. 支持格式同 ipBlockList, 一行一条 |
178 | | ipBlockListFile | []string | 空 | 屏蔽 IP 列表文件. 支持格式同 ipBlockList, 一行一条 |
179 | | genIPDat | uint32 | 0 (禁用) | 1: 生成 IPBlockList.dat. 包括所有被封禁的 Peer IPCIDR, 格式同 ipBlockList; 2: 生成 IPFilter.dat. 包括所有被封禁的 Peer IP; 一行一条 |
180 | | ipUploadedCheck | bool | false (禁用) | IP 上传增量检测. 在满足下列 IP 上传增量 条件后, 会自动屏蔽 Peer |
181 | | ipUpCheckInterval | uint32 | 300 (秒) | IP 上传增量检测/检测间隔. 用于确定上一周期及当前周期, 以比对客户端对 IP 上传增量. 也顺便用于 maxIPPortCount |
182 | | ipUpCheckIncrementMB | uint32 | 38000 (MB) | IP 上传增量检测/增量大小. 若 IP 全局上传增量大小大于设置增量大小, 则允许屏蔽 Peer |
183 | | ipUpCheckPerTorrentRatio | float64 | 3 (X) | IP 上传增量检测/增量倍率. 若 IP 单个 Torrent 上传增量大小大于设置增量倍率及 Torrent 大小之乘积, 则允许屏蔽 Peer |
184 | | maxIPPortCount | uint32 | 0 (禁用) | 每 IP 最大端口数. 若 IP 端口数大于设置值, 会自动屏蔽 Peer |
185 | | banByProgressUploaded | bool | false (禁用) | 增强自动屏蔽 (根据进度及上传量屏蔽 Peer, 未经测试验证). 在满足下列 增强自动屏蔽 条件后, 会自动屏蔽 Peer |
186 | | banByPUStartMB | uint32 | 20 (MB) | 增强自动屏蔽/起始大小. 若客户端上传量大于起始大小, 则允许屏蔽 Peer |
187 | | banByPUStartPrecent | float64 | 2 (%) | 增强自动屏蔽/起始进度. 若客户端上传进度大于设置起始进度, 则允许屏蔽 Peer |
188 | | banByPUAntiErrorRatio | float64 | 3 (X) | 增强自动屏蔽/滞后防误判倍率. 若 Peer 报告下载进度与设置倍率及 Torrent 大小之乘积得到之下载量 比 客户端上传量 还低, 则允许屏蔽 Peer |
189 | | banByRelativeProgressUploaded | bool | false (禁用) | 增强自动屏蔽_相对 (根据相对进度及相对上传量屏蔽 Peer, 未经测试验证). 在满足下列 增强自动屏蔽_相对 条件后, 会自动屏蔽 Peer |
190 | | banByRelativePUStartMB | uint32 | 20 (MB) | 增强自动屏蔽_相对/起始大小. 若客户端相对上传量大于设置起始大小, 则允许屏蔽 Peer |
191 | | banByRelativePUStartPrecent | float64 | 2 (%) | 增强自动屏蔽_相对/起始进度. 若客户端相对上传进度大于设置起始进度, 则允许屏蔽 Peer |
192 | | banByRelativePUAntiErrorRatio | float64 | 3 (X) | 增强自动屏蔽_相对/滞后防误判倍率. 若 Peer 报告相对下载进度与设置倍率之乘积得到之相对下载进度 比 客户端相对上传进度 还低, 则允许屏蔽 Peer |
193 | | ignoreByDownloaded | uint32 | 100 (MB) | 增强自动屏蔽*/最高下载量. 若从 Peer 下载量大于此项, 则跳过增强自动屏蔽 |
194 |
195 | ## 反馈 Feedback
196 | 用户及开发者可以通过 [Issue](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/issues) 反馈 bug, 通过 [Discussion](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/discussions) 提问/讨论/分享 使用方法, 通过 [Pull Request](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/pulls) 向客户端屏蔽器贡献代码改进.
197 | 注意: 应基于 dev 分支. 为 Feature 发起 Pull Request 时, 请不要同步创建 Issue. 由于人手有限, 开发进度可能较为缓慢.
198 |
199 | ## 致谢 Credit
200 |
201 | 1. 我们在客户端屏蔽器的早期开发过程中部分参考了 [jinliming2/qbittorrent-ban-xunlei](https://github.com/jinliming2/qbittorrent-ban-xunlei). 我们可能也会参考其它同类项目对项目进行优化, 但将不在此处单独列出;
202 | 2. 我们会在每期版本的 Release Note 中感谢当期通过 Pull Request 向客户端屏蔽器贡献代码改进的用户及开发者;
203 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
2 | github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
3 | github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg=
4 | github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs=
5 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
6 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
7 | github.com/bdwyertech/go-get-proxied v0.0.0-20221029171534-ea033ac5f9fa h1:fRI5TE3nkZNCFCiwx4f8pn9EqosU63GdpRr/oMZfXaU=
8 | github.com/bdwyertech/go-get-proxied v0.0.0-20221029171534-ea033ac5f9fa/go.mod h1:gguiiQiki8o2ByWTy5cN07JJdBJKvVlxp1GmovszCXE=
9 | github.com/bdwyertech/go-scutil v0.0.0-20210306002117-b25267f54e45 h1:h0VnajBq78VxrXh3qvakigCyjh9pWnTVYkzsLiKrZog=
10 | github.com/bdwyertech/go-scutil v0.0.0-20210306002117-b25267f54e45/go.mod h1:gV303gJocqRNJVrg/n4cB+ZwoHxKKMiAiwe+t627Kjw=
11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12 | github.com/darren/gpac v0.0.0-20210609082804-b56d6523a3af h1:hRl8yeesLVvIFWsUXGv7nysRriS1cYagFvYSRXDKU/g=
13 | github.com/darren/gpac v0.0.0-20210609082804-b56d6523a3af/go.mod h1:pF2H/Bu76N23ydpIIYwMYE8S1dCi9ZoSOC91fPtn44g=
14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
18 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
19 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
20 | github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d h1:enuVjS1vVnToj/GuGZ7QegOAIh1jF340Sg6NXcoMohs=
21 | github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
22 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
23 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
24 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
25 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
26 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
27 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
28 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
29 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
30 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
31 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
32 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
33 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
34 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
35 | github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
36 | github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
37 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
38 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
39 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
40 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
41 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
42 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
43 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
44 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
45 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
48 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
49 | github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
50 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
51 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
52 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
53 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
54 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
55 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
58 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
59 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
60 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
61 | github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY=
62 | github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
64 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
65 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
66 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
67 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
68 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
69 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
70 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
71 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
72 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
73 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
74 | github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
75 | github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
76 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
77 | golang.design/x/hotkey v0.4.1 h1:zLP/2Pztl4WjyxURdW84GoZ5LUrr6hr69CzJFJ5U1go=
78 | golang.design/x/hotkey v0.4.1/go.mod h1:M8SGcwFYHnKRa83FpTFQoZvPO5vVT+kWPztFqTQKmXA=
79 | golang.design/x/mainthread v0.3.0 h1:UwFus0lcPodNpMOGoQMe87jSFwbSsEY//CA7yVmu4j8=
80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
81 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
82 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
83 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
84 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
85 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
86 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
87 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
88 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
89 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
90 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
91 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
92 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
93 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
94 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
96 | golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
97 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
98 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100 | golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
103 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
104 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
105 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
106 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
107 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
108 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
109 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
110 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
111 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
112 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
113 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
114 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
115 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
116 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
117 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
118 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
119 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
120 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
121 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
122 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
123 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
124 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
125 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
126 | gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
127 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
128 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
129 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
130 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
131 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
132 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
133 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
134 |
--------------------------------------------------------------------------------
/console.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "os/signal"
7 | "runtime"
8 | "runtime/debug"
9 | "strconv"
10 | "strings"
11 | "syscall"
12 | "time"
13 | )
14 |
15 | var loopTicker *time.Ticker
16 | var currentTimestamp int64 = 0
17 | var lastCheckUpdateTimestamp int64 = 0
18 | var lastCheckUpdateReleaseVer = ""
19 | var lastCheckUpdateBetaVer = "None"
20 | var githubAPIHeader = map[string]string{"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
21 | var isRunning bool
22 |
23 | type ReleaseStruct struct {
24 | URL string `json:"html_url"`
25 | TagName string `json:"tag_name"`
26 | Name string `json:"name"`
27 | Body string `json:"body"`
28 | PreRelease bool `json:"prerelease"`
29 | }
30 |
31 | func ProcessVersion(version string) (int, int, int, int, string) {
32 | realVersion := strings.SplitN(version, " ", 2)[0]
33 | versionSplit := strings.SplitN(realVersion, ".", 2)
34 |
35 | if strings.Contains(version, "Unknown") {
36 | return -1, 0, 0, 0, ""
37 | }
38 |
39 | if strings.Contains(version, "(Nightly)") {
40 | return -2, 0, 0, 0, ""
41 | }
42 |
43 | if strings.Contains(version, "-") || strings.Contains(version, "_") {
44 | return -3, 0, 0, 0, ""
45 | }
46 |
47 | if len(versionSplit) != 2 {
48 | return -1, 0, 0, 0, ""
49 | }
50 |
51 | mainVersion, err1 := strconv.Atoi(versionSplit[0])
52 |
53 | versionType := 0 // 0: Public, 1: Beta.
54 | versionSplit2 := strings.SplitN(versionSplit[1], "p", 2)
55 | versionSplit3 := strings.SplitN(versionSplit[1], "b", 2)
56 |
57 | subVersionStr := versionSplit[1]
58 | sub2VersionStr := "0"
59 |
60 | if len(versionSplit2) >= 2 {
61 | subVersionStr = versionSplit2[0]
62 | sub2VersionStr = versionSplit2[1]
63 | } else if len(versionSplit3) >= 2 {
64 | versionType = 1
65 | subVersionStr = versionSplit3[0]
66 | sub2VersionStr = versionSplit3[1]
67 | }
68 |
69 | subVersion, err2 := strconv.Atoi(subVersionStr)
70 | sub2Version, err3 := strconv.Atoi(sub2VersionStr)
71 |
72 | if err1 != nil || err2 != nil || err3 != nil {
73 | return -3, 0, 0, 0, ""
74 | }
75 |
76 | return versionType, mainVersion, subVersion, sub2Version, realVersion
77 | }
78 | func CheckUpdate() {
79 | if !config.CheckUpdate || (lastCheckUpdateTimestamp+86400) > currentTimestamp {
80 | return
81 | }
82 |
83 | lastCheckUpdateTimestamp = currentTimestamp
84 |
85 | currentVersionType, currentMainVersion, currentSubVersion, currentSub2Version, currentVersion := ProcessVersion(programVersion)
86 |
87 | if currentVersionType == -1 {
88 | Log("CheckUpdate", GetLangText("CheckUpdate-Ignore_UnknownVersion"), true)
89 | return
90 | }
91 |
92 | if currentVersionType == -2 {
93 | Log("CheckUpdate", GetLangText("CheckUpdate-Ignore_NightlyVersion"), true)
94 | return
95 | }
96 |
97 | if currentVersionType == -3 {
98 | Log("CheckUpdate", GetLangText("CheckUpdate-Ignore_BadVersion"), true, programVersion)
99 | return
100 | }
101 |
102 | listResponseCode, _, listReleaseContent := Fetch("https://api.github.com/repos/Simple-Tracker/qBittorrent-ClientBlocker/releases?per_page=5", false, false, true, &githubAPIHeader)
103 | if listResponseCode == 304 {
104 | return
105 | }
106 |
107 | if listReleaseContent == nil {
108 | Log("CheckUpdate", GetLangText("Error-FetchUpdate"), true)
109 | return
110 | }
111 |
112 | var releasesStruct []ReleaseStruct
113 | if err := json.Unmarshal(listReleaseContent, &releasesStruct); err != nil {
114 | Log("CheckUpdate", GetLangText("Error-Parse"), true, err.Error())
115 | return
116 | }
117 |
118 | matchLatestReleaseVersion := false
119 | matchLatestPreReleaseVersion := false
120 | var latestReleaseStruct ReleaseStruct
121 | var latestPreReleaseStruct ReleaseStruct
122 |
123 | for _, releaseStruct := range releasesStruct {
124 | if releaseStruct.TagName == "" {
125 | continue
126 | }
127 |
128 | if matchLatestPreReleaseVersion && matchLatestReleaseVersion {
129 | break
130 | }
131 |
132 | if !matchLatestPreReleaseVersion && releaseStruct.PreRelease {
133 | matchLatestPreReleaseVersion = true
134 | latestPreReleaseStruct = releaseStruct
135 | }
136 | if !matchLatestReleaseVersion && !releaseStruct.PreRelease {
137 | matchLatestReleaseVersion = true
138 | latestReleaseStruct = releaseStruct
139 | }
140 | }
141 |
142 | hasNewReleaseVersion := false
143 | hasNewPreReleaseVersion := false
144 |
145 | if matchLatestPreReleaseVersion {
146 | if currentVersionType == 1 {
147 | versionType, mainVersion, subVersion, sub2Version, _ := ProcessVersion(latestPreReleaseStruct.TagName)
148 |
149 | if versionType == currentVersionType {
150 | if mainVersion > currentMainVersion {
151 | hasNewPreReleaseVersion = true
152 | } else if mainVersion == currentMainVersion {
153 | if subVersion > currentSubVersion {
154 | hasNewPreReleaseVersion = true
155 | } else if subVersion == currentSubVersion && sub2Version > currentSub2Version {
156 | hasNewPreReleaseVersion = true
157 | }
158 | }
159 | }
160 | }
161 | }
162 |
163 | if matchLatestReleaseVersion {
164 | versionType, mainVersion, subVersion, sub2Version, _ := ProcessVersion(latestReleaseStruct.TagName)
165 |
166 | if versionType == 0 {
167 | if mainVersion > currentMainVersion {
168 | hasNewReleaseVersion = true
169 | } else if mainVersion == currentMainVersion {
170 | if subVersion > currentSubVersion {
171 | hasNewReleaseVersion = true
172 | } else if subVersion == currentSubVersion && sub2Version > currentSub2Version {
173 | hasNewReleaseVersion = true
174 | }
175 | }
176 | }
177 | }
178 |
179 | if latestPreReleaseStruct.TagName == "" {
180 | latestPreReleaseStruct.TagName = "None"
181 | }
182 |
183 | Log("CheckUpdate", GetLangText("CheckUpdate-ShowVersion"), true, currentVersion, latestReleaseStruct.TagName, latestPreReleaseStruct.TagName)
184 |
185 | if hasNewReleaseVersion && lastCheckUpdateReleaseVer != latestReleaseStruct.TagName {
186 | lastCheckUpdateReleaseVer = latestReleaseStruct.TagName
187 | Log("CheckUpdate", GetLangText("CheckUpdate-DetectNewVersion"), true, latestReleaseStruct.TagName, ("https://github.com/Simple-Tracker/" + programName + "/releases/tag/" + latestReleaseStruct.TagName), strings.Replace(latestReleaseStruct.Body, "\r", "", -1))
188 | }
189 |
190 | if hasNewPreReleaseVersion && lastCheckUpdateBetaVer != latestPreReleaseStruct.TagName {
191 | lastCheckUpdateBetaVer = latestPreReleaseStruct.TagName
192 | Log("CheckUpdate", GetLangText("CheckUpdate-DetectNewBetaVersion"), true, latestPreReleaseStruct.TagName, ("https://github.com/Simple-Tracker/" + programName + "/releases/tag/" + latestPreReleaseStruct.TagName), strings.Replace(latestPreReleaseStruct.Body, "\r", "", -1))
193 | }
194 | }
195 | func Task() {
196 | if config.ClientURL == "" {
197 | Log("Task", GetLangText("Error-Task_EmptyURL"), true)
198 | return
199 | }
200 | if !IsSupportClient() {
201 | Log("Task", GetLangText("Error-Task_NotSupportClient"), true, currentClientType)
202 | return
203 | }
204 |
205 | torrents := FetchTorrents()
206 | if torrents == nil {
207 | return
208 | }
209 |
210 | cleanCount := ClearBlockPeer()
211 |
212 | emptyHashCount := 0
213 | noLeechersCount := 0
214 | badTorrentInfoCount := 0
215 | ptTorrentCount := 0
216 |
217 | blockCount := 0
218 | ipBlockCount := 0
219 | badPeersCount := 0
220 | emptyPeersCount := 0
221 |
222 | switch currentClientType {
223 | case "qBittorrent":
224 | torrents2 := torrents.(*[]qB_TorrentStruct)
225 | for _, torrentInfo := range *torrents2 {
226 | ProcessTorrent(torrentInfo.InfoHash, torrentInfo.Tracker, torrentInfo.NumLeechs, torrentInfo.TotalSize, nil, &emptyHashCount, &noLeechersCount, &badTorrentInfoCount, &ptTorrentCount, &blockCount, &ipBlockCount, &badPeersCount, &emptyPeersCount)
227 | }
228 | case "Transmission":
229 | torrents2 := torrents.(*Tr_TorrentsStruct)
230 | for _, torrentInfo := range torrents2.Torrents {
231 | // 手动判断有无 Peer 正在下载.
232 | var leecherCount int64 = 0
233 | for _, torrentPeer := range torrentInfo.Peers {
234 | if torrentPeer.IsUploading {
235 | leecherCount++
236 | }
237 | }
238 |
239 | tracker := ""
240 | if torrentInfo.Private {
241 | tracker = "Private"
242 | }
243 |
244 | ProcessTorrent(torrentInfo.InfoHash, tracker, leecherCount, torrentInfo.TotalSize, torrentInfo.Peers, &emptyHashCount, &noLeechersCount, &badTorrentInfoCount, &ptTorrentCount, &blockCount, &ipBlockCount, &badPeersCount, &emptyPeersCount)
245 | }
246 | case "BitComet":
247 | // BitComet 无法通过 Torrent 列表取得 TorrentInfoHash, 因此使用 TorrentID 取代.
248 | torrents2 := torrents.(*map[int]BC_TorrentStruct)
249 | for torrentID, torrentInfo := range *torrents2 {
250 | var leecherCount int64 = 233
251 | if torrentInfo.UpSpeed > 0 {
252 | leecherCount = 233
253 | }
254 | ProcessTorrent(strconv.Itoa(torrentID), "Unsupported", leecherCount, torrentInfo.TotalSize, nil, &emptyHashCount, &noLeechersCount, &badTorrentInfoCount, &ptTorrentCount, &blockCount, &ipBlockCount, &badPeersCount, &emptyPeersCount)
255 | }
256 | }
257 |
258 | ipBlockCount += CheckAllIP(ipMap, lastIPMap)
259 | torrentBlockCount, torrentIPBlockCount := CheckAllTorrent(torrentMap, lastTorrentMap)
260 | blockCount += torrentBlockCount
261 | ipBlockCount += torrentIPBlockCount
262 |
263 | Log("Debug-Task_IgnoreEmptyHashCount", "%d", false, emptyHashCount)
264 | Log("Debug-Task_IgnoreNoLeechersCount", "%d", false, noLeechersCount)
265 | Log("Debug-Task_IgnorePTTorrentCount", "%d", false, ptTorrentCount)
266 | Log("Debug-Task_IgnoreBadTorrentInfoCount", "%d", false, badTorrentInfoCount)
267 | Log("Debug-Task_IgnoreBadPeersCount", "%d", false, badPeersCount)
268 | Log("Debug-Task_IgnoreEmptyPeersCount", "%d", false, emptyPeersCount)
269 |
270 | if cleanCount != 0 || blockCount != 0 || ipBlockCount != 0 {
271 | if config.GenIPDat == 1 || config.GenIPDat == 2 {
272 | ipfilterCount, ipfilterStr := GenIPFilter(config.GenIPDat, blockPeerMap)
273 | err := SaveIPFilter(ipfilterStr)
274 | if err != "" {
275 | Log("Task", GetLangText("Error-IPFilter_Write"), true, err)
276 | } else {
277 | Log("Task", GetLangText("Success-GenIPFilter"), true, ipfilterCount)
278 | }
279 | }
280 |
281 | SubmitBlockPeer(blockPeerMap)
282 |
283 | iblcLen := 0
284 | ipBlockListCompiled.Range(func(key, value any) bool {
285 | iblcLen++
286 | return true
287 | })
288 |
289 | if !config.IPUploadedCheck && iblcLen <= 0 && len(ipBlockCIDRMapFromSyncServerCompiled) <= 0 {
290 | Log("Task", GetLangText("Task_BanInfo"), true, blockCount, len(blockPeerMap))
291 | } else {
292 | Log("Task", GetLangText("Task_BanInfoWithIP"), true, blockCount, ipBlockCount, len(blockPeerMap))
293 | }
294 | }
295 |
296 | SyncWithServer()
297 | }
298 | func GC() {
299 | ipMapGCCount := (len(ipMap) - 23333333)
300 |
301 | if ipMapGCCount > 0 {
302 | Log("GC", GetLangText("GC_IPMap"), true, ipMapGCCount)
303 | for ip, _ := range ipMap {
304 | ipMapGCCount--
305 | delete(ipMap, ip)
306 | if ipMapGCCount <= 0 {
307 | break
308 | }
309 | }
310 | runtime.GC()
311 | }
312 |
313 | for torrentInfoHash, torrentInfo := range torrentMap {
314 | torrentInfoGCCount := (len(torrentInfo.Peers) - 2333333)
315 | if torrentInfoGCCount > 0 {
316 | Log("GC", GetLangText("GC_TorrentMap"), true, torrentInfoHash, torrentInfoGCCount)
317 | for peerIP, _ := range torrentInfo.Peers {
318 | torrentInfoGCCount--
319 | delete(torrentMap[torrentInfoHash].Peers, peerIP)
320 | if torrentInfoGCCount <= 0 {
321 | break
322 | }
323 | }
324 | runtime.GC()
325 | }
326 | }
327 | }
328 | func WaitStop() {
329 | signalChan := make(chan os.Signal, 1)
330 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM)
331 |
332 | <-signalChan
333 | ReqStop()
334 | }
335 | func ReqStop() {
336 | if !isRunning {
337 | return
338 | }
339 |
340 | Log("ReqStop", GetLangText("ReqStop_Stoping"), true)
341 | isRunning = false
342 | }
343 | func Stop() {
344 | recoverErr := recover()
345 |
346 | if recoverErr != nil {
347 | Log("Stop", GetLangText("Stop_CaughtPanic"), true, recoverErr)
348 | Log("Stop", GetLangText("Stop_StacktraceWhenPanic"), true, string(debug.Stack()))
349 | }
350 |
351 | if loopTicker != nil {
352 | loopTicker.Stop()
353 | }
354 |
355 | DeleteIPFilter()
356 | SubmitBlockPeer(nil)
357 | httpClient.CloseIdleConnections()
358 | httpClientExternal.CloseIdleConnections()
359 | StopServer()
360 | Platform_Stop()
361 |
362 | if recoverErr != nil {
363 | os.Exit(2)
364 | }
365 | }
366 | func RunConsole() {
367 | if startDelay > 0 {
368 | Log("RunConsole", GetLangText("RunConsole_StartDelay"), false, startDelay)
369 | time.Sleep(time.Duration(startDelay) * time.Second)
370 | }
371 |
372 | for !LoadInitConfig(true) {
373 | time.Sleep(2 * time.Second)
374 | if !config.IgnoreFailureExit {
375 | os.Exit(1)
376 | }
377 | }
378 |
379 | isRunning = true
380 |
381 | if config.ExecCommand_Run != "" {
382 | status, out, err := ExecCommand(config.ExecCommand_Run)
383 |
384 | if status {
385 | Log("RunConsole", GetLangText("Success-ExecCommand"), true, out)
386 | } else {
387 | Log("RunConsole", GetLangText("Failed-ExecCommand"), true, out, err)
388 | }
389 | }
390 |
391 | Log("RunConsole", GetLangText("RunConsole_ProgramHasStarted"), true)
392 | loopTicker = time.NewTicker(1 * time.Second)
393 | go WaitStop()
394 | defer Stop()
395 |
396 | for ; true; <-loopTicker.C {
397 | if !isRunning {
398 | break
399 | }
400 |
401 | tmpCurrentTimestamp := time.Now().Unix()
402 | if (currentTimestamp + int64(config.Interval)) <= tmpCurrentTimestamp {
403 | currentTimestamp = tmpCurrentTimestamp
404 | go CheckUpdate()
405 | if LoadInitConfig(false) {
406 | Task()
407 | GC()
408 | }
409 | }
410 | }
411 | }
412 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | # qBittorrent-ClientBlocker
2 |
3 | [中文 (默认, Beta 版本)](README.md) [English (Default, Beta Version)](README.en.md)
4 | [中文 (Public 正式版)](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/blob/master/README.md) [English (Public version)](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/blob/master/README.en.md)
5 |
6 | A client blocker compatible with qBittorrent (4.1+)/Transmission (3.0+, Beta)/BitComet (2.0+, Beta, Partial), which is prohibited to include but not limited to clients such as Xunlei.
7 |
8 | - Support many platforms
9 | - Support log and hot-reload config
10 | - Support ignore private ip
11 | - Support custom blockList (Case-Inensitive, Support regular expression)
12 | - Supports a variety of clients and their authentication, and can automatically detect some clients (Currently supports qBittorrent. Errors that occur when detecting clients can be ignored)
13 | - Support enhanced automatic ban (Default disable): Automatically ban peer based on the default or set related parameter
14 | - Under Windows, support show and hide window through systray or window hotkey (Ctrl+Alt+B) (Some users [feedback](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/issues/10) it may affect ban. Due to unknown reason, the function can be avoided if related problem are encountered)
15 |
16 | Usually, occasional errors can be ignored. The reasons may be: 1. Network problems; 2. Client timeout; 3. Transmission periodically rotates CSRF Token; ...
17 |
18 | 
19 |
20 | ## 使用 Usage
21 |
22 | ### Prerequisite
23 |
24 | - Client Web UI must be enabled.
25 |
26 | ### Use conventional version
27 |
28 | 1. Download compressed from [**GitHub Release**](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/releases) and decompress it;
29 |
30 |
31 | View common platform download version of comparison table
32 |
33 | | OS | Processor Arch | Processor Integers | Download Version | Note |
34 | | -------- | ---------- | ---------- | ------------- | ------------------------------------------------------ |
35 | | macOS | ARM64 | 64-bit | darwin-arm64 | Common in Apple M series |
36 | | macOS | AMD64 | 64-bit | darwin-amd64 | Common in Intel series |
37 | | Windows | AMD64 | 64-bit | windows-amd64 | Common in most modern PC |
38 | | Windows | i386 | 32-bit | windows-386 | Occasionally on some old PC |
39 | | Windows | ARM64 | 64-bit | windows-arm64 | Common on new platform, it's applied to some tablet/notebooks/minority special hardware |
40 | | Windows | ARMv6 | 32-bit | windows-arm | Rare platform, applied to some ancient hardware, such as Surface RT, etc |
41 | | Linux | AMD64 | 64-bit | linux-amd64 | Common NAS and server |
42 | | Linux | i386 | 32-bit | linux-386 | Rarely in some old NAS and server |
43 | | Linux | ARM64 | 64-bit | linux-arm64 | Common server and development board, such as Oracle or Raspberry Pi, etc |
44 | | Linux | ARMv* | 32-bit | linux-armv* | Rarely in some old server and development board, Check /proc/cpuinfo or try which version can run from high to low |
45 |
46 | Other versions of Linux/Netbsd/FreeBSD/OpenBSD/Solaris can use this form as an example, and select one that suits you in the list.
47 |
48 |
49 | 2. After decompression, you may need to modify the attached config file ```config.json```;
50 |
51 | - You can set config according to high-level needs. See [配置 Config](#配置-config).
52 | - If blocker runs on this machine, but client "Skip client certification" is disabled (and password is not empty and does not manually set password in blocker), you must modify config file and fill in ```clientPassword```.
53 | - If blocker is not running on this machine or client is not installed on the default path or using blocker with 2.4 and below version, config file must be modified and fills in ```clientURL```/```clientUsername```/```clientPassword```.
54 |
55 | 3. Start blocker and observe whether the information output is normal;
56 |
57 | For Windows, you can choose shortcut of client, put your own blocker path, and run client and blocker at the same time;
58 |
59 | qBittorrent: ```C:\Windows\System32\cmd.exe /c "(tasklist | findstr qBittorrent-ClientBlocker || start C:\Users\Example\qBittorrent-ClientBlocker\qBittorrent-ClientBlocker.exe) && start qbittorrent.exe"```
60 |
61 | For macOS, You can choose a basic [LaunchAgent](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/wiki#launchagent-macos) for starting from OS start and background run;
62 |
63 | For Linux, You can choose a basic [Systemd service](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/wiki#systemd-linux) for starting from OS start and background run;
64 |
65 | ### Use Docker version
66 |
67 | - Pull image from [**Docker Hub**](https://hub.docker.com/r/simpletracker/qbittorrent-clientblocker).
68 |
69 | ```
70 | docker pull simpletracker/qbittorrent-clientblocker:latest
71 | ```
72 |
73 | - Configuration method 1: Use config file mapping
74 |
75 | 1. Create a new ```config.json``` in right location, as a configuration file, the specific config can refer [config.json](config.json) and [配置 Config](#配置-config);
76 |
77 | 2. Fills in ```clientURL```/```clientUsername```/```clientPassword```;
78 |
79 | - You can set config according to high-level needs. See [配置 Config](#配置-config).
80 | - If client "IP subnet whitelist" is enabled, you don't need fill in ```clientUsername``` and ```clientPassword```.
81 |
82 | 3. Run docker image and view log to observe whether the information output is normal;
83 |
84 | The following command templates are used as a reference only, please replace ```/path/config.json``` to your config path (The file should be created before mounting).
85 |
86 | ```
87 | docker run -d \
88 | --name=qbittorrent-clientblocker --network=bridge --restart unless-stopped \
89 | -v /path/config.json:/app/config.json \
90 | simpletracker/qbittorrent-clientblocker:latest
91 | ```
92 |
93 | - Configuration method 1: Use environment variable
94 |
95 | - Prerequisite: Set the ```useENV``` environment variable is ```true```.
96 | - Use environment variables to configure settings on demand. For details, see [配置 Config](#配置-config).
97 | - If config is complicated, blockList may not take effect. Therefore, if you need to configure this setting, it's not recommended to use environment variable.
98 | - The following command templates are used as a reference only.
99 |
100 | ```
101 | docker run -d \
102 | --name=qbittorrent-clientblocker --network=bridge --restart unless-stopped \
103 | -e useENV=true \
104 | -e debug=false \
105 | -e logPath=logs \
106 | -e blockList='["ExampleBlockList1", "ExampleBlockList2"]' \
107 | -e clientURL=http://example.com \
108 | -e clientUsername=exampleUser \
109 | -e clientPassword=examplePass \
110 | simpletracker/qbittorrent-clientblocker:latest
111 | ```
112 |
113 | ## 参数 Flag
114 |
115 | | Parameter | Default | Note |
116 | | ----- | ----- | ----- |
117 | | -v/--version | false | Show program version and exit |
118 | | -c/--config | config.json | Config path |
119 | | -ca/--config_additional | config_additional.json | Additional config path |
120 | | --debug | false | Debug mode. Effective before loading config file |
121 | | --startdelay | 0 (Sec, Disable) | Start delay. Special uses for some user |
122 | | --nochdir | false | Don't change working directory. Change to the program directory by default |
123 | | --reghotkey | true | Register window hotkey. Only available on Windows. The window hotkey is fixed to CTRL+ALT+B |
124 | | --hidewindow | false | Hide window by default. Only available on Windows |
125 | | --hidesystray | false | Hide systray by default. Only available on Windows |
126 |
127 | ## 配置 Config
128 |
129 | Docker version is configured through the same name variable configuration, which actually is implemented by automatically conversion environment variable as config file.
130 |
131 | | Parameter | Type | Default | Note |
132 | | ----- | ----- | ----- | ----- |
133 | | checkUpdate | bool | true | Check update. Automatically checks for update by default |
134 | | debug | bool | false | Debug mode. Enable you can see more information, but it may disrupt the field of vision |
135 | | debug_CheckTorrent | string | false | Debug mode (CheckTorrent, must enable debug). If it's enabled, debug info will include each Torrent Hash, but the amount of information will be large |
136 | | debug_CheckPeer | string | false | Debug mode (CheckPeer, must enable debug). If it's enabled, debug info will include each Torrent Peer, but the amount of information will be large |
137 | | interval | uint32 | 6 (秒) | Ban Check Interval (Hot-reload is not supported). Each cycle interval will obtain relevant information from backend for judgment and blocking. Short interval can help reduce ban time but may cause client to freeze, but Long interval can help reduce CPU usage |
138 | | cleanInterval | uint32 | 3600 (Sec) | Clean blocked peer interval. Short interval will cause expired Peer to be unblocked faster after blocking duration is reached, but Long interval will help merge and clean up expired Peer log |
139 | | updateInterval | uint32 | 86400 (Sec) | List URL update interval (blockListURL/ipBlockListURL). Reasonable intervals help improve update efficiency and reduce network usage |
140 | | restartInterval | uint32 | 6 (Sec) | Restart Torrent interval. This is a measure to solve the problem that the blocklist of some clients (Transmission) cannot be effective. It is implemented by restarting Torrent. Too short an interval may cause the blocking to be ineffective |
141 | | torrentMapCleanInterval | uint32 | 60 (Sec) | Torrent Map Clean Interval (Only useful after enable ipUploadedCheck+ipUpCheckPerTorrentRatio/banByRelativeProgressUploaded, It's also the judgment interval). Short interval can make judgments more frequent but may cause delayed misjudgments |
142 | | banTime | uint32 | 86400 (Sec) | Ban duration. Short interval will cause peer to be unblocked faster |
143 | | banAllPort | bool | true | Block IP all port. Enabled by default and setting is not currently supported |
144 | | banIPCIDR | string | /32 | Block IPv4 CIDR. Used to expand Peer’s block IP range |
145 | | banIP6CIDR | string | /128 | Block IPv6 CIDR. Used to expand Peer’s block IP range |
146 | | ignoreEmptyPeer | bool | true | Ignore peers without PeerID and ClientName. Usually occurs on clients where connection is not fully established |
147 | | ignoreNoLeechersTorrent | bool | false | Ignore torrent without leechers. Enabling may improve performance, but may cause inaccuracies with some clients (such as qBittorrent) |
148 | | ignorePTTorrent | bool | true | Ignore PT Torrent. If the main Tracker contains ```?passkey=```/```?authkey=```/```?secure=```/```A string of 32 digits consisting of uppercase and lowercase char or/and number``` |
149 | | ignoreFailureExit | bool | false | Ignore failure exit. If enabled, it will continue to retry after first detection of the client fails or authentication fails |
150 | | sleepTime | uint32 | 20 (MicroSec) | Query waiting time of each Torrent Peers. Short interval can make blocking Peer faster but may cause client lag, Long interval can help average CPU usage |
151 | | timeout | uint32 | 6 (MillSec) | Request timeout. If interval is too short, peer may not be properly blocked. If interval is too long, timeout request will affect blocking other peer |
152 | | proxy | string | Auto | Use proxy. Still automatically detect proxy on first load. Empty: Disable proxy; Auto: Automatic (use proxy only for external requests); All: Use proxy for all requests |
153 | | longConnection | bool | true | Long connection. Enable to reduce resource consumption |
154 | | logPath | string | logs | Log path. Must enable logToFile |
155 | | logToFile | bool | true | Log general information to file. If enabled, it can be used for general analysis and statistical purposes |
156 | | logDebug | bool | false | Log debug information to file (Must enable debug and logToFile). If enabled, it can be used for advanced analysis and statistical purposes, but the amount of information is large |
157 | | listen | string | 127.0.0.1:26262 | Listen port. Used to provide BlockPeerList to some client (Transmission). For non-localhost, you can change to ```:``` |
158 | | clientType | string | Empty | Client type. Available Web API or RPC address. Prerequisite for using blocker, if client config file cannot be automatically detect, must be filled in correctly. Currently support ```qBittorrent```/```Transmission```/```BitComet``` |
159 | | clientURL | string | Empty | Client address. Prerequisite for using blocker, if client config file cannot be automatically read, must be filled in correctly. Prefix must specify http or https protocol, such as ```http://127.0.0.1:990``` or ```http://127.0.0.1:9091/transmission/rpc``` |
160 | | clientUsername | string | Empty | Client username. Leaving it blank will skip authentication. If you enable client "Skip local client authentication", you can leave it blank by default, because the client config file can be automatically read and set |
161 | | clientPassword | string | Empty | Client password. If client "Skip local client authentication" is enabled, it can be left blank by default |
162 | | useBasicAuth | bool | false | Also authenticates via HxTTP Basic Auth. Suitable for backend that only support Basic Auth (such as Transmission/BitComet) or add/change authentication methods via reverse proxy, etc |
163 | | useShadowBan | bool | true | Use ShadowBan API to ban peer. Only works with clients that support ShadowBan API (such as qBEE). If the relevant API is not available when loading the configuration, it will be automatically disabled |
164 | | skipCertVerification | bool | false | Skip Web API certificate verification. Suitable for self-signed and expired certificates |
165 | | fetchFailedThreshold | int | 0 (Disable) | Maximum number of fetch failures. When set number is exceeded, execCommand_FetchFailed will be executed |
166 | | execCommand_FetchFailed | string | Empty | Execute external command (FetchFailed). First parameter is regarded as an external program path, execute the command when number of fetch failures exceeds set threshold |
167 | | execCommand_Run | string | Empty | Execute external command (Run). First parameter is regarded as an external program path, execute the command when program starts |
168 | | execCommand_Ban | string | Empty | Execute external command (Ban). First parameter is regarded as an external program path, and each parameter should be separated by ```\|```, command can use ```{peerIP}```/```{peerPort}```/```{torrentInfoHash}``` to use related info (peerPort=-1 means ban all port) |
169 | | execCommand_Ban | string | Empty | Execute external command (Ban). First parameter is regarded as an external program path, and each parameter should be separated by ```\|```, command can use ```{peerIP}```/```{peerPort}```/```{torrentInfoHash}``` to use related info (peerPort=-1 means ban all port) |
170 | | execCommand_Unban | string | Empty | Execute external command (Unban). First parameter is regarded as an external program path, and each parameter should be separated by ```\|```, command can use ```{peerIP}```/```{peerPort}```/```{torrentInfoHash}``` to use related info (peerPort=-1 means ban all port) |
171 | | syncServerURL | string | Empty | Sync server URL. Sync server will submit TorrentMap to server and receive BlockIPCIDR from server |
172 | | syncServerToken | string | Empty | Sync server Token. Some sync server may require authentication |
173 | | blockList | []string | Empty (Included in config.json) | Block client list. Judge PeerID or ClientName at the same time, case-insensitive, support regular expression |
174 | | blockListURL | []string | Empty | Block client list URL. Support format is same as blockList, one rule per line |
175 | | blockListFile | []string | Empty | Block client list File. Support format is same as blockList, one rule per line |
176 | | portBlockList | []uint32 | Empty | Block port list. If peer port matches any of ports, Peer will be automatically block |
177 | | ipBlockList | []string | Empty | Block IP list. Support excluding ports IP (1.2.3.4) or IPCIDR (2.3.3.3/3) |
178 | | ipBlockListURL | []string | Empty | Block IP list URL. Support format is same as ipBlockList, one rule per line |
179 | | ipBlockListFile | []string | Empty | Block IP list File. Support format is same as ipBlockList, one rule per line |
180 | | genIPDat | uint32 | 0 (Disable) | 1: Generate IPBlockList.dat. Include All Peer IPCIDR, support format is same as ipBlockList; 2: Generate IPFilter.dat. Include All Peer IP; One rule per line |
181 | | ipUploadedCheck | bool | false | IP upload incremental detection. After the following IP upload incremental conditions are met, Peer will be automatically block |
182 | | ipUpCheckInterval | uint32 | 300 (Sec) | IP upload incremental detection/Interval. Used to determine the previous cycle and the current cycle to compare Peer's IP upload increment. It is also used for maxIPPortCount |
183 | | ipUpCheckIncrementMB | uint32 | 38000 (MB) | IP upload incremental detection/Increment size. If the IP global upload increment size is greater than the set increment size, Peer will be automatically block |
184 | | ipUpCheckPerTorrentRatio | float64 | 3 (X) | IP upload incremental detection/Increment ratio. If the IP single torrent upload increment size is greater than the product of the set increment ratio and the torrent size, Peer will be automatically block |
185 | | maxIPPortCount | uint32 | 0 (Disable) | Maximum number of ports per IP. If the number of IP ports is greater than the set value, Peer will be automatically block |
186 | | banByProgressUploaded | bool | false | Enhanced automatic blocking (blocking Peer based on progress and uploaded, not verified by testing). After the following enhanced automatic blocking conditions are met, Peer will be automatically blocked |
187 | | banByPUStartMB | uint32 | 20 (MB) | Enhanced automatic blocking/Start size. If the client uploaded is greater than the set initial size, Peer will be automatically block |
188 | | banByPUStartPrecent | float64 | 2 (%) | Enhanced automatic blocking/Start progress. If the client upload progress is greater than the set start progress, Peer will be automatically block |
189 | | banByPUAntiErrorRatio | float64 | 3 (X) | Enhanced automatic blocking/Lag anti-misjudgment ratio. If the downloaded obtained by the Peer's reported download progress multiplied by the set ratio and the torrent size is lower than Peer's uploaded, Peer will be automatically block |
190 | | banByRelativeProgressUploaded | bool | false | Enhanced automatic blocking_Relative (Block Peer based on relative progress and relative uploaded, not verified by testing). After the following Enhanced automatic blocking_Relative conditions are met, Peer will be automatically block |
191 | | banByRelativePUStartMB | uint32 | 20 (MB) | Enhanced automatic blocking_Relative/Start size. If the relative uploaded of the client is greater than the set start size, Peer will be automatically block |
192 | | banByRelativePUStartPrecent | float64 | 2 (%) | Enhanced automatic blocking_Relative/Start progress. If the relative upload progress of the client is greater than the set start progress, Peer will be automatically block |
193 | | banByRelativePUAntiErrorRatio | float64 | 3 (X) | Enhanced automatic blocking_Relative/Lag anti-misjudgment ratio. If the relative download progress obtained by the product of the relative download progress reported by the peer and the set ratio is lower than the relative upload progress of the client, Peer will be automatically block |
194 | | ignoreByDownloaded | uint32 | 100 (MB) | Enhanced automatic blocking*/Max downloaded. If downloaded from Peer is greater than this value, enhanced automatic blocking will be skipped |
195 |
196 | ## 反馈 Feedback
197 | User and developer can report bug through [Issue](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/issues), ask/discuss/share usage through [Discussion](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/discussions), contribute code improvement to blocker through [Pull Request](https://github.com/Simple-Tracker/qBittorrent-ClientBlocker/pulls).
198 | Note: Should be based on dev branch. When opening a Pull Request for a Feature, please do not create an Issue simultaneously.
199 |
200 | ## 致谢 Credit
201 |
202 | 1. We partially referenced [jinliming2/qbittorrent-ban-xunlei](https://github.com/jinliming2/qbittorrent-ban-xunlei) during early development of blocker. We may also refer to other similar project to optimize this project, but they will not be listed separately here;
203 | 2. We will thank the user and developer who contributed code improvement to blocker through Pull Request in Release Note;
204 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "encoding/json"
6 | "flag"
7 | "log"
8 | "net/http"
9 | "net/http/cookiejar"
10 | "os"
11 | "path/filepath"
12 | "reflect"
13 | "strings"
14 | "sync"
15 | "time"
16 |
17 | "github.com/dlclark/regexp2"
18 | "github.com/pelletier/go-toml/v2"
19 | "github.com/tidwall/jsonc"
20 | )
21 |
22 | type ConfigStruct struct {
23 | CheckUpdate bool
24 | Debug bool
25 | Debug_CheckTorrent bool
26 | Debug_CheckPeer bool
27 | Interval uint32
28 | CleanInterval uint32
29 | UpdateInterval uint32
30 | RestartInterval uint32
31 | TorrentMapCleanInterval uint32
32 | BanTime uint32
33 | BanAllPort bool
34 | BanIPCIDR string
35 | BanIP6CIDR string
36 | IgnoreEmptyPeer bool
37 | IgnoreNoLeechersTorrent bool
38 | IgnorePTTorrent bool
39 | IgnoreFailureExit bool
40 | SleepTime uint32
41 | Timeout uint32
42 | Proxy string
43 | LongConnection bool
44 | LogPath string
45 | LogToFile bool
46 | LogDebug bool
47 | Listen string
48 | ClientType string
49 | ClientURL string
50 | ClientUsername string
51 | ClientPassword string
52 | UseBasicAuth bool
53 | UseShadowBan bool
54 | SkipCertVerification bool
55 | FetchFailedThreshold int
56 | ExecCommand_FetchFailed string
57 | ExecCommand_Run string
58 | ExecCommand_Ban string
59 | ExecCommand_Unban string
60 | SyncServerURL string
61 | SyncServerToken string
62 | BlockList []string
63 | BlockListURL []string
64 | BlockListFile []string
65 | PortBlockList []uint32
66 | IPBlockList []string
67 | IPBlockListURL []string
68 | IPBlockListFile []string
69 | IgnoreByDownloaded uint32
70 | GenIPDat uint32
71 | IPUploadedCheck bool
72 | IPUpCheckInterval uint32
73 | IPUpCheckIncrementMB uint32
74 | IPUpCheckPerTorrentRatio float64
75 | MaxIPPortCount uint32
76 | BanByProgressUploaded bool
77 | BanByPUStartMB uint32
78 | BanByPUStartPrecent float64
79 | BanByPUAntiErrorRatio float64
80 | BanByRelativeProgressUploaded bool
81 | BanByRelativePUStartMB uint32
82 | BanByRelativePUStartPrecent float64
83 | BanByRelativePUAntiErrorRatio float64
84 | }
85 |
86 | var programName = "qBittorrent-ClientBlocker"
87 | var programVersion = "Unknown"
88 | var programUserAgent = programName + "/" + programVersion
89 |
90 | var shortFlag_ShowVersion bool
91 | var longFlag_ShowVersion bool
92 | var startDelay uint
93 | var noChdir bool
94 | var needRegHotKey bool
95 | var needHideWindow bool
96 | var needHideSystray bool
97 |
98 | var randomStrRegexp = regexp2.MustCompile("[a-zA-Z0-9]{32}", 0)
99 | var blockListCompiled sync.Map
100 | var ipBlockListCompiled sync.Map
101 | var blockListURLLastFetch int64 = 0
102 | var ipBlockListURLLastFetch int64 = 0
103 | var blockListFileLastMod = make(map[string]int64)
104 | var ipBlockListFileLastMod = make(map[string]int64)
105 | var cookieJar, _ = cookiejar.New(nil)
106 |
107 | var lastURL = ""
108 | var configLastMod = make(map[string]int64)
109 | var configFilename string = "config.json"
110 | var shortFlag_configFilename string
111 | var longFlag_configFilename string
112 | var additionConfigFilename string = "config_additional.json"
113 | var shortFlag_additionConfigFilename string
114 | var longFlag_additionConfigFilename string
115 |
116 | var httpTransport = &http.Transport{
117 | DisableKeepAlives: true,
118 | ForceAttemptHTTP2: false,
119 | MaxConnsPerHost: 32,
120 | MaxIdleConns: 32,
121 | MaxIdleConnsPerHost: 32,
122 | IdleConnTimeout: 60 * time.Second,
123 | TLSHandshakeTimeout: 12 * time.Second,
124 | ResponseHeaderTimeout: 60 * time.Second,
125 | TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
126 | Proxy: GetProxy,
127 | }
128 |
129 | var httpClient http.Client
130 | var httpClientExternal http.Client // 没有 Cookie.
131 |
132 | var httpServer = http.Server{
133 | ReadTimeout: 30,
134 | WriteTimeout: 30,
135 | Handler: &httpServerHandler{},
136 | }
137 |
138 | var config = ConfigStruct{
139 | CheckUpdate: true,
140 | Debug: false,
141 | Debug_CheckTorrent: false,
142 | Debug_CheckPeer: false,
143 | Interval: 6,
144 | CleanInterval: 3600,
145 | UpdateInterval: 86400,
146 | RestartInterval: 6,
147 | TorrentMapCleanInterval: 60,
148 | BanTime: 86400,
149 | BanAllPort: false,
150 | BanIPCIDR: "/32",
151 | BanIP6CIDR: "/128",
152 | IgnoreEmptyPeer: true,
153 | IgnoreNoLeechersTorrent: false,
154 | IgnorePTTorrent: true,
155 | IgnoreFailureExit: false,
156 | SleepTime: 20,
157 | Timeout: 6,
158 | Proxy: "Auto",
159 | LongConnection: true,
160 | LogPath: "logs",
161 | LogToFile: true,
162 | LogDebug: false,
163 | Listen: "127.0.0.1:26262",
164 | ClientType: "",
165 | ClientURL: "",
166 | ClientUsername: "",
167 | ClientPassword: "",
168 | UseBasicAuth: false,
169 | UseShadowBan: true,
170 | SkipCertVerification: false,
171 | FetchFailedThreshold: 0,
172 | ExecCommand_FetchFailed: "",
173 | ExecCommand_Run: "",
174 | ExecCommand_Ban: "",
175 | ExecCommand_Unban: "",
176 | SyncServerURL: "",
177 | SyncServerToken: "",
178 | BlockList: []string{},
179 | BlockListURL: []string{},
180 | BlockListFile: []string{},
181 | PortBlockList: []uint32{},
182 | IPBlockList: []string{},
183 | IPBlockListURL: nil,
184 | IPBlockListFile: nil,
185 | IgnoreByDownloaded: 100,
186 | GenIPDat: 0,
187 | IPUploadedCheck: false,
188 | IPUpCheckInterval: 300,
189 | IPUpCheckIncrementMB: 38000,
190 | IPUpCheckPerTorrentRatio: 3,
191 | MaxIPPortCount: 0,
192 | BanByProgressUploaded: false,
193 | BanByPUStartMB: 20,
194 | BanByPUStartPrecent: 2,
195 | BanByPUAntiErrorRatio: 3,
196 | BanByRelativeProgressUploaded: false,
197 | BanByRelativePUStartMB: 20,
198 | BanByRelativePUStartPrecent: 2,
199 | BanByRelativePUAntiErrorRatio: 3,
200 | }
201 |
202 | func SetBlockListFromContent(blockListContent []string, blockListSource string) int {
203 | setCount := 0
204 |
205 | for index, content := range blockListContent {
206 | content = StrTrim(ProcessRemark(content))
207 | if content == "" {
208 | Log("Debug-SetBlockListFromContent_Compile", GetLangText("Error-Debug-EmptyLineWithSource"), false, index, blockListSource)
209 | continue
210 | }
211 |
212 | if _, exists := blockListCompiled.Load(content); exists {
213 | continue
214 | }
215 |
216 | Log("Debug-SetBlockListFromContent_Compile", ":%d %s (Source: %s)", false, index, content, blockListSource)
217 |
218 | reg, err := regexp2.Compile("(?i)"+content, 0)
219 | if err != nil {
220 | Log("SetBlockListFromContent_Compile", GetLangText("Error-SetBlockListFromContent_Compile"), true, index, content, blockListSource)
221 | continue
222 | }
223 |
224 | reg.MatchTimeout = 50 * time.Millisecond
225 |
226 | blockListCompiled.Store(content, reg)
227 | setCount++
228 | }
229 |
230 | return setCount
231 | }
232 | func SetBlockListFromFile() bool {
233 | if config.BlockListFile == nil || len(config.BlockListFile) == 0 {
234 | return true
235 | }
236 |
237 | setCount := 0
238 |
239 | for _, filePath := range config.BlockListFile {
240 | blockListFileStat, err := os.Stat(filePath)
241 | if err != nil {
242 | Log("SetBlockListFromFile", GetLangText("Error-LoadFile"), false, filePath, err.Error())
243 | return false
244 | }
245 |
246 | // Max 8MB.
247 | if blockListFileStat.Size() > 8388608 {
248 | Log("SetBlockListFromFile", GetLangText("Error-LargeFile"), true)
249 | continue
250 | }
251 |
252 | fileLastMod := blockListFileStat.ModTime().Unix()
253 | if fileLastMod == blockListFileLastMod[filePath] {
254 | return false
255 | }
256 | if blockListFileLastMod[filePath] != 0 {
257 | Log("Debug-SetBlockListFromFile", GetLangText("Debug-SetBlockListFromFile_HotReload"), false, filePath)
258 | }
259 |
260 | blockListContent, err := os.ReadFile(filePath)
261 | if err != nil {
262 | Log("SetBlockListFromFile", GetLangText("Error-LoadFile"), true, filePath, err.Error())
263 | return false
264 | }
265 |
266 | blockListFileLastMod[filePath] = fileLastMod
267 |
268 | var content []string
269 | if filepath.Ext(filePath) == ".json" {
270 | err = json.Unmarshal(jsonc.ToJSON(blockListContent), &content)
271 | if err != nil {
272 | Log("SetBlockListFromFile", GetLangText("Error-GenJSONWithID"), true, filePath, err.Error())
273 | continue
274 | }
275 | } else {
276 | content = strings.Split(string(blockListContent), "\n")
277 | }
278 |
279 | setCount += SetBlockListFromContent(content, filePath)
280 | }
281 |
282 | Log("SetBlockListFromFile", GetLangText("Success-SetBlockListFromFile"), true, setCount)
283 | return true
284 | }
285 | func SetBlockListFromURL() bool {
286 | if config.BlockListURL == nil || len(config.BlockListURL) == 0 || (blockListURLLastFetch+int64(config.UpdateInterval)) > currentTimestamp {
287 | return true
288 | }
289 |
290 | blockListURLLastFetch = currentTimestamp
291 | setCount := 0
292 |
293 | for _, blockListURL := range config.BlockListURL {
294 | httpStatusCode, httpHeader, blockListContent := Fetch(blockListURL, false, false, true, nil)
295 | if httpStatusCode == 304 {
296 | continue
297 | }
298 |
299 | if blockListContent == nil {
300 | //blockListURLLastFetch -= (int64(config.UpdateInterval) + 900)
301 | Log("SetBlockListFromURL", GetLangText("Error-FetchResponse2"), true)
302 | continue
303 | }
304 |
305 | // Max 8MB.
306 | if len(blockListContent) > 8388608 {
307 | Log("SetBlockListFromURL", GetLangText("Error-LargeFile"), true)
308 | continue
309 | }
310 |
311 | var content []string
312 | if strings.HasSuffix(strings.ToLower(strings.Split(httpHeader.Get("Content-Type"), ";")[0]), "json") {
313 | err := json.Unmarshal(jsonc.ToJSON(blockListContent), &content)
314 | if err != nil {
315 | Log("SetBlockListFromFile", GetLangText("Error-GenJSONWithID"), true, blockListURL, err.Error())
316 | continue
317 | }
318 | } else {
319 | content = strings.Split(string(blockListContent), "\n")
320 | }
321 |
322 | setCount += SetBlockListFromContent(content, blockListURL)
323 | }
324 |
325 | Log("SetBlockListFromURL", GetLangText("Success-SetBlockListFromURL"), true, setCount)
326 | return true
327 | }
328 | func SetIPBlockListFromContent(ipBlockListContent []string, ipBlockListSource string) int {
329 | setCount := 0
330 |
331 | for index, content := range ipBlockListContent {
332 | content = StrTrim(ProcessRemark(content))
333 | if content == "" {
334 | Log("Debug-SetIPBlockListFromContent_Compile", GetLangText("Error-Debug-EmptyLineWithSource"), false, index, ipBlockListSource)
335 | continue
336 | }
337 |
338 | if _, exists := ipBlockListCompiled.Load(content); exists {
339 | continue
340 | }
341 |
342 | Log("Debug-SetIPBlockListFromContent_Compile", ":%d %s (Source: %s)", false, index, content, ipBlockListSource)
343 | cidr := ParseIPCIDR(content)
344 | if cidr == nil {
345 | Log("SetIPBlockListFromContent_Compile", GetLangText("Error-SetIPBlockListFromContent_Compile"), true, index, content, ipBlockListSource)
346 | continue
347 | }
348 |
349 | ipBlockListCompiled.Store(content, cidr)
350 | setCount++
351 | }
352 |
353 | return setCount
354 | }
355 | func SetIPBlockListFromFile() bool {
356 | if config.IPBlockListFile == nil || len(config.IPBlockListFile) == 0 {
357 | return true
358 | }
359 |
360 | setCount := 0
361 |
362 | for _, filePath := range config.IPBlockListFile {
363 | ipBlockListFileStat, err := os.Stat(filePath)
364 | if err != nil {
365 | Log("SetIPBlockListFromFile", GetLangText("Error-LoadFile"), false, filePath, err.Error())
366 | return false
367 | }
368 |
369 | fileLastMod := ipBlockListFileStat.ModTime().Unix()
370 | if fileLastMod <= ipBlockListFileLastMod[filePath] {
371 | return true
372 | }
373 |
374 | if ipBlockListFileLastMod[filePath] != 0 {
375 | Log("Debug-SetIPBlockListFromFile", GetLangText("Debug-SetIPBlockListFromFile_HotReload"), false, filePath)
376 | }
377 |
378 | ipBlockListFile, err := os.ReadFile(filePath)
379 | if err != nil {
380 | Log("SetIPBlockListFromFile", GetLangText("Error-LoadFile"), true, filePath, err.Error())
381 | return false
382 | }
383 |
384 | ipBlockListFileLastMod[filePath] = fileLastMod
385 |
386 | var content []string
387 | if filepath.Ext(filePath) == ".json" {
388 | err := json.Unmarshal(jsonc.ToJSON(ipBlockListFile), &content)
389 | if err != nil {
390 | Log("SetIPBlockListFromFile", GetLangText("Error-GenJSONWithID"), true, filePath, err.Error())
391 | }
392 | } else {
393 | content = strings.Split(string(ipBlockListFile), "\n")
394 | }
395 |
396 | setCount += SetIPBlockListFromContent(content, filePath)
397 | }
398 |
399 | Log("SetIPBlockListFromFile", GetLangText("Success-SetIPBlockListFromFile"), true, setCount)
400 | return true
401 | }
402 | func SetIPBlockListFromURL() bool {
403 | if config.IPBlockListURL == nil || len(config.IPBlockListURL) == 0 || (ipBlockListURLLastFetch+int64(config.UpdateInterval)) > currentTimestamp {
404 | return true
405 | }
406 |
407 | ipBlockListURLLastFetch = currentTimestamp
408 | setCount := 0
409 |
410 | for _, ipBlockListURL := range config.IPBlockListURL {
411 | httpStatusCode, httpHeader, ipBlockListContent := Fetch(ipBlockListURL, false, false, true, nil)
412 | if httpStatusCode == 304 {
413 | continue
414 | }
415 |
416 | if ipBlockListContent == nil {
417 | //ipBlockListURLLastFetch -= (int64(config.UpdateInterval) + 900)
418 | Log("SetIPBlockListFromURL", GetLangText("Error-FetchResponse2"), true)
419 | continue
420 | }
421 |
422 | if len(ipBlockListContent) > 8388608 {
423 | Log("SetIPBlockListFromURL", GetLangText("Error-LargeFile"), true)
424 | continue
425 | }
426 |
427 | var content []string
428 | if strings.HasSuffix(httpHeader.Get("Content-Type"), "json") {
429 | err := json.Unmarshal(jsonc.ToJSON(ipBlockListContent), &content)
430 | if err != nil {
431 | Log("SetIPBlockListFromURL", GetLangText("Error-GenJSONWithID"), true, ipBlockListURL, err.Error())
432 | continue
433 | }
434 | } else {
435 | content = strings.Split(string(ipBlockListContent), "\n")
436 | }
437 |
438 | setCount += SetIPBlockListFromContent(content, ipBlockListURL)
439 | }
440 |
441 | Log("SetIPBlockListFromURL", GetLangText("Success-SetIPBlockListFromURL"), true, setCount)
442 |
443 | return true
444 | }
445 | func LoadConfig(filename string, notExistErr bool) int {
446 | configFileStat, err := os.Stat(filename)
447 | if err != nil {
448 | notExist := os.IsNotExist(err)
449 | if notExistErr || !notExist {
450 | Log("Debug-LoadConfig", GetLangText("Error-LoadConfigMeta"), false, filename, err.Error())
451 | }
452 | if notExist {
453 | return -5
454 | }
455 |
456 | return -2
457 | }
458 |
459 | tmpConfigLastMod := configFileStat.ModTime().Unix()
460 | if tmpConfigLastMod <= configLastMod[filename] {
461 | return -1
462 | }
463 |
464 | if configLastMod[filename] != 0 {
465 | Log("Debug-LoadConfig", GetLangText("Debug-LoadConfig_HotReload"), false, filename)
466 | }
467 |
468 | configFile, err := os.ReadFile(filename)
469 | if err != nil {
470 | Log("LoadConfig", GetLangText("Error-LoadConfig"), true, filename, err.Error())
471 | return -3
472 | }
473 |
474 | configLastMod[filename] = tmpConfigLastMod
475 |
476 | switch filepath.Ext(strings.ToLower(filename)) {
477 | case ".json":
478 | if err := json.Unmarshal(jsonc.ToJSON(configFile), &config); err != nil {
479 | Log("LoadConfig", GetLangText("Error-ParseConfig"), true, filename, err.Error())
480 | return -4
481 | }
482 | case ".toml":
483 | if err := toml.Unmarshal(configFile, &config); err != nil {
484 | Log("LoadConfig", GetLangText("Error-ParseConfig"), true, filename, err.Error())
485 | return -4
486 | }
487 | }
488 |
489 | Log("LoadConfig", GetLangText("Success-LoadConfig"), true, filename)
490 |
491 | return 0
492 | }
493 | func InitConfig() {
494 | if config.Interval < 1 {
495 | config.Interval = 1
496 | }
497 |
498 | if config.Timeout < 1 {
499 | config.Timeout = 1
500 | }
501 |
502 | if config.ClientURL != "" {
503 | config.ClientURL = strings.TrimRight(config.ClientURL, "/")
504 | }
505 |
506 | if config.SkipCertVerification {
507 | httpTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
508 | } else {
509 | httpTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: false}
510 | }
511 |
512 | httpTransportExternal := httpTransport.Clone()
513 |
514 | if config.Proxy == "Auto" {
515 | // Aka default. 仅对外部资源使用代理.
516 | httpTransport.Proxy = nil
517 | httpTransportExternal.Proxy = GetProxy
518 | } else if config.Proxy == "All" {
519 | httpTransport.Proxy = GetProxy
520 | httpTransportExternal.Proxy = GetProxy
521 | } else {
522 | httpTransport.Proxy = nil
523 | httpTransportExternal.Proxy = nil
524 | }
525 |
526 | if config.LongConnection {
527 | httpTransport.DisableKeepAlives = false
528 | }
529 |
530 | currentTimeout := time.Duration(config.Timeout) * time.Second
531 |
532 | httpClient = http.Client{
533 | Timeout: currentTimeout,
534 | Jar: cookieJar,
535 | Transport: httpTransport,
536 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
537 | return http.ErrUseLastResponse
538 | },
539 | }
540 |
541 | httpClientExternal = http.Client{
542 | Timeout: currentTimeout,
543 | Transport: httpTransportExternal,
544 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
545 | return http.ErrUseLastResponse
546 | },
547 | }
548 |
549 | httpServer.ReadTimeout = currentTimeout
550 | httpServer.WriteTimeout = currentTimeout
551 |
552 | t := reflect.TypeOf(config)
553 | v := reflect.ValueOf(config)
554 | for k := 0; k < t.NumField(); k++ {
555 | Log("LoadConfig_Current", "%v: %v", false, t.Field(k).Name, v.Field(k).Interface())
556 | }
557 |
558 | EraseSyncMap(&blockListCompiled)
559 | blockListURLLastFetch = 0
560 | SetBlockListFromContent(config.BlockList, "BlockList")
561 |
562 | EraseSyncMap(&ipBlockListCompiled)
563 | ipBlockListURLLastFetch = 0
564 | SetIPBlockListFromContent(config.IPBlockList, "IPBlockList")
565 | }
566 | func LoadInitConfig(firstLoad bool) bool {
567 | loadConfigStatus := LoadConfig(configFilename, true)
568 |
569 | if loadConfigStatus < -1 {
570 | Log("LoadInitConfig", GetLangText("Failed-LoadInitConfig"), true)
571 | } else {
572 | loadAdditionalConfigStatus := LoadConfig(additionConfigFilename, false)
573 | if loadAdditionalConfigStatus == -5 && additionConfigFilename == "config_additional.json" {
574 | loadAdditionalConfigStatus = LoadConfig("config/"+additionConfigFilename, false)
575 | }
576 |
577 | if loadConfigStatus == 0 || loadAdditionalConfigStatus == 0 {
578 | InitConfig()
579 | }
580 | }
581 |
582 | if !LoadLog() && logFile != nil {
583 | logFile.Close()
584 | logFile = nil
585 | }
586 |
587 | if firstLoad {
588 | GetProxy(nil)
589 | SetURLFromClient()
590 | }
591 |
592 | if config.ClientURL != "" {
593 | if lastURL != config.ClientURL {
594 | if !DetectClient() {
595 | Log("LoadInitConfig", GetLangText("LoadInitConfig_DetectClientFailed"), true)
596 | return false
597 | }
598 | if !Login() {
599 | Log("LoadInitConfig", GetLangText("LoadInitConfig_AuthFailed"), true)
600 | return false
601 | }
602 | InitClient()
603 | SubmitBlockPeer(nil)
604 | lastURL = config.ClientURL
605 | }
606 | } else {
607 | // 重置为上次使用的 URL, 主要目的是防止热重载配置文件可能破坏首次启动后从 qBittorrent 配置文件读取的 URL.
608 | config.ClientURL = lastURL
609 | }
610 |
611 | if config.UseShadowBan && TestShadowBanAPI() <= 0 {
612 | config.UseShadowBan = false
613 | }
614 |
615 | if !firstLoad {
616 | SetBlockListFromFile()
617 | SetIPBlockListFromFile()
618 | go SetBlockListFromURL()
619 | go SetIPBlockListFromURL()
620 | }
621 |
622 | return true
623 | }
624 | func RegFlag() {
625 | flag.BoolVar(&shortFlag_ShowVersion, "v", false, GetLangText("ProgramVersion"))
626 | flag.BoolVar(&longFlag_ShowVersion, "version", false, GetLangText("ProgramVersion"))
627 | flag.StringVar(&shortFlag_configFilename, "c", "", GetLangText("ConfigPath"))
628 | flag.StringVar(&longFlag_configFilename, "config", "", GetLangText("ConfigPath"))
629 | flag.StringVar(&shortFlag_additionConfigFilename, "ca", "", GetLangText("AdditionalConfigPath"))
630 | flag.StringVar(&longFlag_additionConfigFilename, "config_additional", "", GetLangText("AdditionalConfigPath"))
631 | flag.BoolVar(&config.Debug, "debug", false, GetLangText("DebugMode"))
632 | flag.UintVar(&startDelay, "startdelay", 0, GetLangText("StartDelay"))
633 | flag.BoolVar(&noChdir, "nochdir", false, GetLangText("NoChdir"))
634 | flag.BoolVar(&needRegHotKey, "reghotkey", true, GetLangText("RegHotKey"))
635 | flag.BoolVar(&needHideWindow, "hidewindow", false, GetLangText("HideWindow"))
636 | flag.BoolVar(&needHideSystray, "hidesystray", false, GetLangText("HideSystray"))
637 | flag.Parse()
638 | }
639 | func ShowVersion() {
640 | Log("ShowVersion", "%s %s", false, programName, programVersion)
641 | }
642 | func PrepareEnv() bool {
643 | LoadLang(GetLangCode())
644 | RegFlag()
645 | ShowVersion()
646 | log.SetFlags(0)
647 | log.SetOutput(logwriter)
648 |
649 | if shortFlag_ShowVersion || longFlag_ShowVersion {
650 | return false
651 | }
652 |
653 | if longFlag_configFilename != "" {
654 | configFilename = longFlag_configFilename
655 | } else if shortFlag_configFilename != "" {
656 | configFilename = shortFlag_configFilename
657 | }
658 |
659 | if longFlag_additionConfigFilename != "" {
660 | additionConfigFilename = longFlag_additionConfigFilename
661 | } else if shortFlag_additionConfigFilename != "" {
662 | additionConfigFilename = shortFlag_additionConfigFilename
663 | }
664 |
665 | path, err := os.Executable()
666 | if err != nil {
667 | Log("PrepareEnv", GetLangText("Error-DetectProgramPath"), false, err.Error())
668 | return false
669 | }
670 |
671 | if !noChdir {
672 | programDir := filepath.Dir(path)
673 | if os.Chdir(programDir) == nil {
674 | Log("PrepareEnv", GetLangText("Success-ChangeWorkingDir"), false, programDir)
675 | LoadLang(GetLangCode())
676 | } else {
677 | Log("PrepareEnv", GetLangText("Failed-ChangeWorkingDir"), false, programDir)
678 | }
679 | }
680 |
681 | return true
682 | }
683 |
--------------------------------------------------------------------------------