Skip to content

Commit 57811dc

Browse files
author
Simple-Tracker
committed
Improve & Add WebUI & Add BTN (Not yet tested)
1 parent 05d23b3 commit 57811dc

38 files changed

Lines changed: 3915 additions & 417 deletions

BTN.go

Lines changed: 695 additions & 0 deletions
Large diffs are not rendered by default.

BTN_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
)
8+
9+
func TestGetTorrentIdentifierIsStableAndCaseInsensitive(t *testing.T) {
10+
id1 := GetTorrentIdentifier("ABCDEF0123456789")
11+
id2 := GetTorrentIdentifier("abcdef0123456789")
12+
13+
if id1 != id2 {
14+
t.Fatalf("GetTorrentIdentifier should be case-insensitive: %q != %q", id1, id2)
15+
}
16+
if len(id1) != 64 {
17+
t.Fatalf("identifier length=%d, want 64", len(id1))
18+
}
19+
if id1 == "abcdef0123456789" {
20+
t.Fatal("identifier should not equal the raw torrent hash")
21+
}
22+
}
23+
24+
func TestBTNGetConfigLoadsConfiguration(t *testing.T) {
25+
oldClientExternal := httpClientExternal
26+
oldConfig := config
27+
oldBtnConfig := btnConfig
28+
oldLastGetConfig := btn_lastGetConfig
29+
oldCurrentTimestamp := currentTimestamp
30+
oldIsGetting := btn_isGettingConfig.Load()
31+
defer func() {
32+
httpClientExternal = oldClientExternal
33+
config = oldConfig
34+
btnConfig = oldBtnConfig
35+
btn_lastGetConfig = oldLastGetConfig
36+
currentTimestamp = oldCurrentTimestamp
37+
btn_isGettingConfig.Store(oldIsGetting)
38+
}()
39+
40+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
41+
if got := r.Header.Get("Authorization"); got != "Bearer app-a@secret-a" {
42+
t.Fatalf("Authorization=%q, want Bearer app-a@secret-a", got)
43+
}
44+
w.Header().Set("Content-Type", "application/json")
45+
_, _ = w.Write([]byte(`{
46+
"min_protocol_version": 1,
47+
"max_protocol_version": 3,
48+
"ability": {
49+
"submit_peers": {
50+
"interval": 10,
51+
"endpoint": "https://btn.example/submit_peers",
52+
"random_initial_delay": 0,
53+
"version": "v1"
54+
}
55+
}
56+
}`))
57+
}))
58+
defer server.Close()
59+
60+
httpClientExternal = *server.Client()
61+
config = oldConfig
62+
config.BTNConfigureURL = server.URL
63+
config.BTNAppID = "app-a"
64+
config.BTNAppSecret = "secret-a"
65+
btnConfig = nil
66+
btn_lastGetConfig = 0
67+
currentTimestamp = 100
68+
btn_isGettingConfig.Store(false)
69+
70+
BTN_GetConfig()
71+
72+
if btnConfig == nil {
73+
t.Fatal("BTN_GetConfig should populate btnConfig")
74+
}
75+
if btnConfig.MinMainVersion != 1 || btnConfig.MaxMainVersion != 3 {
76+
t.Fatalf("unexpected protocol range: min=%d max=%d", btnConfig.MinMainVersion, btnConfig.MaxMainVersion)
77+
}
78+
ability, ok := btnConfig.Ability["submit_peers"]
79+
if !ok {
80+
t.Fatal("submit_peers ability should exist")
81+
}
82+
if ability.Endpoint != "https://btn.example/submit_peers" {
83+
t.Fatalf("Endpoint=%q, want https://btn.example/submit_peers", ability.Endpoint)
84+
}
85+
if btn_lastGetConfig != 100 {
86+
t.Fatalf("btn_lastGetConfig=%d, want 100", btn_lastGetConfig)
87+
}
88+
}

README.en.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ Docker version is configured through the same name variable configuration, which
155155
| logToFile | bool | true | Log general information to file. If enabled, it can be used for general analysis and statistical purposes |
156156
| 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 |
157157
| listen | string | 127.0.0.1:26262 | Listen port. Used to provide BlockPeerList to some client (Transmission). For non-localhost, you can change to ```<Host>:<Port>``` |
158+
| webUI | bool | false | Enable the built-in WebUI. It exposes runtime status, recent blocks, and recent logs on a separate listener |
159+
| webUIListen | string | 127.0.0.1:7222 | WebUI listening address. If you expose it outside localhost, it is recommended to set ```webUIUsername``` and ```webUIPassword``` |
160+
| webUIUsername | string | Empty | WebUI Basic Auth username. When this field is empty, WebUI auth is disabled and ```webUIPassword``` is ignored |
161+
| webUIPassword | string | Empty | WebUI Basic Auth password. Only HTTP Basic Auth is provided; HTTPS is not included |
158162
| 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``` |
159163
| 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``` |
160164
| 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 |
@@ -166,10 +170,17 @@ Docker version is configured through the same name variable configuration, which
166170
| 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 |
167171
| execCommand_Run | string | Empty | Execute external command (Run). First parameter is regarded as an external program path, execute the command when program starts |
168172
| 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) |
170173
| 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) |
171174
| syncServerURL | string | Empty | Sync server URL. Sync server will submit TorrentMap to server and receive BlockIPCIDR from server |
172175
| syncServerToken | string | Empty | Sync server Token. Some sync server may require authentication |
176+
| btnConfigureURL | string | Empty | BTN server URL. Currently not available, [What is BTN?](https://docs.pbh-btn.com/en/docs/btn/intro/) |
177+
| btnAppID | string | Empty | BTN AppID. Some BTN servers may require authentication |
178+
| btnAppSecret | string | Empty | BTN AppSecret. Some BTN servers may require authentication |
179+
| btnSyncRules | bool | true (Enable) | Sync BTN server cloud ban rules |
180+
| btnSyncException | bool | true (Enable) | Sync BTN server cloud exception rules (whitelist) |
181+
| btnSubmitPeers | bool | false (Disable) | Submit Peer snapshots to BTN server |
182+
| btnSubmitBans | bool | false (Disable) | Submit local ban records to BTN server |
183+
| btnSubmitHistories | bool | false (Disable) | Submit Peer traffic delta history snapshots to BTN server |
173184
| blockList | []string | Empty (Included in config.json) | Block client list. Judge PeerID or ClientName at the same time, case-insensitive, support regular expression |
174185
| blockListURL | []string | Empty | Block client list URL. Support format is same as blockList, one rule per line |
175186
| blockListFile | []string | Empty | Block client list File. Support format is same as blockList, one rule per line |

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ Docker 版本通过相同名称的环境变量配置, 通过自动转换环境
155155
| logToFile | bool | true (启用) | 记录普通信息到日志. 启用后可用于一般的分析及统计用途 |
156156
| logDebug | bool | false (禁用) | 记录调试信息到日志 (须先启用 debug 及 logToFile). 启用后可用于进阶的分析及统计用途, 但信息量较大 |
157157
| listen | string | 127.0.0.1:26262 | 监听端口. 用于向部分客户端 (Transmission) 提供 BlockPeerList. 非本机使用可改为 ```<Host>:<Port>``` |
158+
| webUI | bool | false (禁用) | 启用内置 WebUI. 可查看运行状态及最近日志 |
159+
| webUIListen | string | 127.0.0.1:7222 | WebUI 监听地址. 若暴露到非本机网络, 建议配合 ```webUIUsername```/```webUIPassword``` 使用 |
160+
| webUIUsername | string | 空 | WebUI Basic Auth 用户名. 留空时, WebUI 不启用认证 |
161+
| webUIPassword | string | 空 | WebUI Basic Auth 密码 |
158162
| clientType | string | 空 | 客户端类型. 使用客户端屏蔽器的前提条件, 若未能自动检测客户端类型, 则须正确填入. 目前支持 ```qBittorrent```/```Transmission```/```BitComet``` |
159163
| clientURL | string | 空 | 客户端地址. 可用 Web API 或 RPC 地址. 使用客户端屏蔽器的前提条件, 若未能自动读取客户端配置文件, 则须正确填入. 前缀必须指定 http 或 https 协议, 如 ```http://127.0.0.1:990/api``` 或 ```http://127.0.0.1:9091/transmission/rpc``` |
160164
| clientUsername | string | 空 | 客户端账号. 留空会跳过认证. 若启用客户端内 "跳过本机客户端认证" 可默认留空, 因可自动读取客户端配置文件并设置 |
@@ -169,6 +173,14 @@ Docker 版本通过相同名称的环境变量配置, 通过自动转换环境
169173
| execCommand_Unban | string | 空 | 执行外部命令 (Unban). 首个参数被视作外部程序路径, 各参数均应使用 ```\|``` 分割, 命令可以使用 ```{peerIP}```/```{peerPort}```/```{torrentInfoHash}``` 来使用相关信息 (peerPort=-1 意味着全端口封禁) |
170174
| syncServerURL | string | 空 | 同步服务器 URL. 同步服务器会将 TorrentMap 提交至服务器, 并从服务器接收屏蔽 IPCIDR 列表 |
171175
| syncServerToken | string | 空 | 同步服务器 Token. 部分同步服务器可能需要认证 |
176+
| btnConfigureURL | string | 空 | BTN 服务器 URL. 目前尚不可用, [什么是 BTN?](https://docs.pbh-btn.com/docs/btn/intro/) |
177+
| btnAppID | string | 空 | BTN AppID. 部分 BTN 服务器可能需要认证 |
178+
| btnAppSecret | string | 空 | BTN AppSecret. 部分 BTN 服务器可能需要认证 |
179+
| btnSyncRules | bool | true (启用) | 同步 BTN 服务器的云端封禁规则 |
180+
| btnSyncException | bool | true (启用) | 同步 BTN 服务器的云端例外规则 (白名单) |
181+
| btnSubmitPeers | bool | false (禁用) | 上报 Peer 快照至 BTN 服务器 |
182+
| btnSubmitBans | bool | false (禁用) | 上报本地封禁记录至 BTN 服务器 |
183+
| btnSubmitHistories | bool | false (禁用) | 上报 Peer 流量增量历史至 BTN 服务器 |
172184
| blockList | []string | 空 (于 config.json 附带) | 屏蔽客户端列表. 同时判断 PeerID 及 ClientName, 不区分大小写, 支持正则表达式 |
173185
| blockListURL | []string | 空 | 屏蔽客户端列表 URL. 支持格式同 blockList, 一行一条 |
174186
| blockListFile | []string | 空 | 屏蔽客户端列表文件. 支持格式同 blockList, 一行一条 |

SyncServer.go

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"github.com/tidwall/jsonc"
66
"net"
7+
"sync"
8+
"sync/atomic"
79
)
810

911
type SyncServer_ConfigStruct struct {
@@ -18,17 +20,25 @@ type SyncServer_SubmitStruct struct {
1820
TorrentMap map[string]TorrentInfoStruct `json:"torrentMap"`
1921
}
2022

21-
var syncServer_isSubmiting bool = false
23+
type SyncServer_RuleStruct struct {
24+
Net *net.IPNet
25+
Reason string
26+
}
27+
28+
var syncServer_isSubmiting atomic.Bool
2229
var syncServer_lastSync int64 = 0
2330
var syncServer_syncConfig = SyncServer_ConfigStruct{
2431
Interval: 60,
2532
Status: "",
2633
BlockIPRule: make(map[string][]string),
2734
}
28-
var ipBlockCIDRMapFromSyncServerCompiled = make(map[string]*net.IPNet)
35+
var syncServer_CompiledRules []SyncServer_RuleStruct
36+
var ipBlockCIDRMapMutex sync.RWMutex
2937

3038
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})
39+
torrentMapMutex.RLock()
40+
syncJSON, err := json.Marshal(SyncServer_SubmitStruct{Version: 1, Timestamp: atomic.LoadInt64(&currentTimestamp), Token: config.SyncServerToken, TorrentMap: torrentMap})
41+
torrentMapMutex.RUnlock()
3242
if err != nil {
3343
Log("SyncWithServer_PrepareJSON", GetLangText("Error-GenJSON"), true, err.Error())
3444
return false, ""
@@ -38,13 +48,12 @@ func SyncWithServer_PrepareJSON(torrentMap map[string]TorrentInfoStruct) (bool,
3848
}
3949
func SyncWithServer_Submit(syncJSON string) bool {
4050
_, _, syncServerContent := Submit(config.SyncServerURL, syncJSON, false, false, nil)
41-
syncServer_isSubmiting = false
4251
if syncServerContent == nil {
4352
Log("SyncWithServer", GetLangText("Error-FetchResponse2"), true)
4453
return false
4554
}
4655

47-
// Max 8MB.
56+
// 最大 8MB.
4857
if len(syncServerContent) > 8388608 {
4958
Log("SyncWithServer", GetLangText("Error-LargeFile"), true)
5059
return false
@@ -60,7 +69,7 @@ func SyncWithServer_Submit(syncJSON string) bool {
6069
return false
6170
}
6271

63-
tmpIPBlockCIDRMapFromSyncServerCompiled := make(map[string]*net.IPNet)
72+
var tmpSyncServerCompiledRules []SyncServer_RuleStruct
6473

6574
for reason, ipArr := range syncServer_syncConfig.BlockIPRule {
6675
logReason := false
@@ -72,11 +81,6 @@ func SyncWithServer_Submit(syncJSON string) bool {
7281
continue
7382
}
7483

75-
if cidr, exists := ipBlockCIDRMapFromSyncServerCompiled[ipBlockListLine]; exists {
76-
tmpIPBlockCIDRMapFromSyncServerCompiled[ipBlockListLine] = cidr
77-
continue
78-
}
79-
8084
Log("Debug-SyncWithServer_Compile", ":%d %s", false, ipBlockListLineNum, ipBlockListLine)
8185
cidr := ParseIPCIDR(ipBlockListLine)
8286
if cidr == nil {
@@ -89,25 +93,43 @@ func SyncWithServer_Submit(syncJSON string) bool {
8993
Log("SyncWithServer", GetLangText("SyncWithServer_Compile-BlockByReason"), true, reason)
9094
}
9195

92-
tmpIPBlockCIDRMapFromSyncServerCompiled[ipBlockListLine] = cidr
96+
tmpSyncServerCompiledRules = append(tmpSyncServerCompiledRules, SyncServer_RuleStruct{Net: cidr, Reason: reason})
9397
Log("SyncWithServer_BlockCIDR", ":%d %s", false, ipBlockListLineNum, ipBlockListLine)
9498
}
9599
}
96100

97-
ipBlockCIDRMapFromSyncServerCompiled = tmpIPBlockCIDRMapFromSyncServerCompiled
101+
ipBlockCIDRMapMutex.Lock()
102+
syncServer_CompiledRules = tmpSyncServerCompiledRules
103+
ipBlockCIDRMapMutex.Unlock()
98104

99-
Log("Debug-SyncWithServer", GetLangText("Success-SyncWithServer"), true, len(ipBlockCIDRMapFromSyncServerCompiled))
105+
Log("Debug-SyncWithServer", GetLangText("Success-SyncWithServer"), true, len(syncServer_CompiledRules))
100106
return true
101107
}
108+
109+
func SyncServer_CheckPeer(ipObj net.IP) (bool, string) {
110+
if ipObj == nil {
111+
return false, ""
112+
}
113+
114+
ipBlockCIDRMapMutex.RLock()
115+
defer ipBlockCIDRMapMutex.RUnlock()
116+
117+
for _, rule := range syncServer_CompiledRules {
118+
if rule.Net != nil && rule.Net.Contains(ipObj) {
119+
return true, "Bad-IP_FromSyncServer (" + rule.Reason + ")"
120+
}
121+
}
122+
123+
return false, ""
124+
}
102125
func SyncWithServer_FullSubmit(syncJSON string) bool {
103-
syncServer_isSubmiting = true
104126
syncStatus := SyncWithServer_Submit(syncJSON)
105-
syncServer_isSubmiting = false
127+
syncServer_isSubmiting.Store(false)
106128

107129
return syncStatus
108130
}
109131
func SyncWithServer() bool {
110-
if config.SyncServerURL == "" || (syncServer_lastSync+int64(syncServer_syncConfig.Interval)) > currentTimestamp || syncServer_isSubmiting {
132+
if config.SyncServerURL == "" || (atomic.LoadInt64(&syncServer_lastSync)+int64(syncServer_syncConfig.Interval)) > atomic.LoadInt64(&currentTimestamp) || syncServer_isSubmiting.Load() {
111133
return true
112134
}
113135

@@ -118,9 +140,13 @@ func SyncWithServer() bool {
118140
return false
119141
}
120142

121-
syncServer_lastSync = currentTimestamp
143+
atomic.StoreInt64(&syncServer_lastSync, atomic.LoadInt64(&currentTimestamp))
122144

123-
go SyncWithServer_FullSubmit(syncJSON)
145+
if syncServer_isSubmiting.CompareAndSwap(false, true) {
146+
GoWithCrashLog("SyncWithServer_FullSubmit", func() {
147+
SyncWithServer_FullSubmit(syncJSON)
148+
})
149+
}
124150

125151
return true
126152
}

0 commit comments

Comments
 (0)