├── .gitmodules ├── .vscode └── c_cpp_properties.json ├── README.md ├── main.hpp ├── LICENSE └── main.cpp /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rapidjson"] 2 | path = rapidjson 3 | url = https://github.com/Tencent/rapidjson.git 4 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/g++", 10 | "cStandard": "c11", 11 | "cppStandard": "c++17", 12 | "intelliSenseMode": "clang-x64" 13 | } 14 | ], 15 | "version": 4 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qBittorrent Ban Xunlei 2 | 3 | 通过 WebUI API 为 qBittorrent server 屏蔽吸血迅雷。 4 | 5 | ## Build 6 | 7 | ```bash 8 | $ apt install build-essential libcurl4-openssl-dev 9 | $ git clone ...... 10 | $ cd ...... 11 | $ git submodule init 12 | $ git submodule update --depth 1 13 | $ # 编辑 main.hpp 文件第 5 行,改为 qbittorrent 本地地址 14 | $ # qbittorrent 需要开启设置 Web UI 中的 Bypass authentication for clients on localhost,以跳过本地接口请求的帐号认证 15 | $ g++ main.cpp -lcurl -o qbittorrent-ban-xl 16 | ``` 17 | 18 | ## TODO 19 | 20 | * [ ] 支持配置文件,配置 qbittorrent 地址、封禁时长等 21 | * [ ] 支持配置帐号密码 22 | * [ ] 添加 systemd 守护运行配置文件示例 23 | * [ ] 添加 Docker 运行方式 24 | -------------------------------------------------------------------------------- /main.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #define HOST "http://127.0.0.1:8080" 6 | #define COOKIEFILE "./qbittorrent.cookie" 7 | 8 | #define BANTIME 86400 9 | 10 | std::regex XL0012("-XL0012-", std::regex_constants::icase); 11 | std::regex XUNLEI001("Xunlei 0\\.0\\.1\\.", std::regex_constants::icase); 12 | 13 | std::regex regCOLON(":"); 14 | 15 | struct torrent_info { 16 | int num_leechs; 17 | uint64_t size; 18 | }; 19 | 20 | static std::map banned_list; 21 | static std::map torrent_list; 22 | 23 | void do_job(); 24 | 25 | // 清除过期封禁列表 26 | void clear_expired_ban_list(); 27 | 28 | // 更新 Torrent 列表 29 | void update_torrents(); 30 | 31 | 32 | // 获取指定 Torrent 的 Peer 列表,并识别封禁 33 | void update_peers(const std::string&, const uint64_t&); 34 | 35 | // 设置封禁列表 36 | void set_ban_list(); 37 | 38 | // CURL 读取数据处理函数 39 | size_t CURL_write_stdString(void*, size_t, size_t, std::string*); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Liming Jin 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 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include "rapidjson/include/rapidjson/document.h" 6 | 7 | #include "main.hpp" 8 | 9 | using std::chrono::seconds; 10 | using std::chrono::system_clock; 11 | using std::this_thread::sleep_for; 12 | 13 | bool notChangedFlag = true; 14 | 15 | int main() { 16 | while (true) { 17 | try { 18 | do_job(); 19 | } catch(std::string e) { 20 | std::cout << "[E]" << ' ' << e << std::endl; 21 | } catch(const char *e) { 22 | std::cout << "[E]" << ' ' << e << std::endl; 23 | } 24 | sleep_for(seconds(3)); 25 | } 26 | 27 | return 0; 28 | } 29 | 30 | void do_job() { 31 | clear_expired_ban_list(); 32 | update_torrents(); 33 | for (auto &m : torrent_list) { 34 | update_peers(m.first, m.second.size); 35 | } 36 | if (!notChangedFlag) { 37 | set_ban_list(); 38 | } 39 | } 40 | 41 | // 清除过期封禁列表 42 | void clear_expired_ban_list() { 43 | std::time_t now = system_clock::to_time_t(system_clock::now()); 44 | 45 | std::string cleared; 46 | 47 | for (const auto &m : banned_list) { 48 | if (m.second <= now) { 49 | banned_list.erase(m.first); 50 | cleared += m.first + " "; 51 | notChangedFlag = false; 52 | } 53 | } 54 | 55 | if (!notChangedFlag) { 56 | std::cout << "[I] Cleared: " << cleared << std::endl; 57 | } 58 | } 59 | 60 | // 更新 Torrent 列表 61 | void update_torrents() { 62 | static int rid = 0; 63 | 64 | CURL *curl = curl_easy_init(); 65 | 66 | if (!curl) { 67 | throw "update_torrents: CURL init failure!"; 68 | } 69 | 70 | std::string str; 71 | 72 | struct curl_slist *chunk = NULL; 73 | chunk = curl_slist_append(chunk, "Accept: application/json"); 74 | 75 | curl_easy_setopt(curl, CURLOPT_URL, (std::string(HOST) + "/api/v2/sync/maindata?rid=" + std::to_string(rid)).c_str()); 76 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); 77 | curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); 78 | curl_easy_setopt(curl, CURLOPT_COOKIEFILE, COOKIEFILE); 79 | curl_easy_setopt(curl, CURLOPT_COOKIEJAR, COOKIEFILE); 80 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CURL_write_stdString); 81 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &str); 82 | // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1l); 83 | 84 | CURLcode res = curl_easy_perform(curl); 85 | 86 | curl_easy_cleanup(curl); 87 | curl_slist_free_all(chunk); 88 | 89 | if (res != CURLE_OK) { 90 | throw std::string("update_torrents: CURL request failure! ") + curl_easy_strerror(res); 91 | } 92 | 93 | if (str.length() < 2) { 94 | throw std::string("update_torrents: Cannot fetch data! ") + str; 95 | } 96 | 97 | rapidjson::Document document; 98 | document.Parse(str.c_str()); 99 | rid = document["rid"].GetInt(); 100 | 101 | auto torrents = document.FindMember("torrents"); 102 | if (torrents == document.MemberEnd() || !torrents->value.IsObject()) { 103 | return; 104 | } 105 | 106 | for (const auto &m : torrents->value.GetObject()) { 107 | std::string name = m.name.GetString(); 108 | auto torrent = m.value.GetObject(); 109 | auto num_leechs = torrent.FindMember("num_leechs"); 110 | bool has_num_leechs = num_leechs != torrent.MemberEnd() && num_leechs->value.IsNumber(); 111 | auto size = torrent.FindMember("size"); 112 | bool has_size = size != torrent.MemberEnd() && size->value.IsUint64(); 113 | 114 | auto info = torrent_list.find(name); 115 | if (info == torrent_list.end()) { 116 | struct torrent_info new_info = { 117 | .num_leechs = has_num_leechs ? num_leechs->value.GetInt() : 0, 118 | .size = has_size ? size->value.GetUint64() : INT_MAX, 119 | }; 120 | torrent_list[name] = new_info; 121 | } else { 122 | if (has_num_leechs) { 123 | info->second.num_leechs = num_leechs->value.GetInt(); 124 | } 125 | if (has_size) { 126 | info->second.size = size->value.GetUint64(); 127 | } 128 | } 129 | } 130 | 131 | auto removed = document.FindMember("torrents_removed"); 132 | if (removed != document.MemberEnd() && removed->value.IsArray()) { 133 | for (const auto &m : document["torrents_removed"].GetArray()) { 134 | torrent_list.erase(m.GetString()); 135 | } 136 | } 137 | } 138 | 139 | // 获取正在传输的 Torrent 列表 140 | void update_peers(const std::string &hash, const uint64_t &size) { 141 | CURL *curl = curl_easy_init(); 142 | 143 | if (!curl) { 144 | throw "update_peers: CURL init failure!"; 145 | } 146 | 147 | std::string str; 148 | 149 | struct curl_slist *chunk = NULL; 150 | chunk = curl_slist_append(chunk, "Accept: application/json"); 151 | 152 | curl_easy_setopt(curl, CURLOPT_URL, (std::string(HOST) + "/api/v2/sync/torrentPeers?hash=" + hash + "&rid=0").c_str()); 153 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); 154 | curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); 155 | curl_easy_setopt(curl, CURLOPT_COOKIEFILE, COOKIEFILE); 156 | curl_easy_setopt(curl, CURLOPT_COOKIEJAR, COOKIEFILE); 157 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CURL_write_stdString); 158 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &str); 159 | // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1l); 160 | 161 | CURLcode res = curl_easy_perform(curl); 162 | 163 | curl_easy_cleanup(curl); 164 | curl_slist_free_all(chunk); 165 | 166 | if (res != CURLE_OK) { 167 | throw std::string("update_peers: CURL request failure! ") + curl_easy_strerror(res); 168 | } 169 | 170 | if (str.length() < 2) { 171 | throw std::string("update_peers: Cannot fetch data! ") + str; 172 | } 173 | 174 | rapidjson::Document document; 175 | document.Parse(str.c_str()); 176 | 177 | std::time_t expire = system_clock::to_time_t(system_clock::now()) + BANTIME; 178 | 179 | auto peers = document.FindMember("peers"); 180 | if (peers == document.MemberEnd() || !peers->value.IsObject()) { 181 | return; 182 | } 183 | 184 | for (const auto &m : peers->value.GetObject()) { 185 | auto peer = m.value.GetObject(); 186 | 187 | auto ip = peer.FindMember("ip"); 188 | if (ip == peer.MemberEnd() || !ip->value.IsString()) { 189 | continue; 190 | } 191 | 192 | std::string ip_address = ip->value.GetString(); 193 | 194 | auto client = peer.FindMember("client"); 195 | if (client != peer.MemberEnd() && client->value.IsString()) { 196 | std::string client_str = client->value.GetString(); 197 | if (std::regex_search(client_str, XL0012) || std::regex_search(client_str, XUNLEI001)) { 198 | banned_list[ip_address] = expire; 199 | notChangedFlag = false; 200 | continue; 201 | } 202 | } 203 | 204 | auto uploaded = peer.FindMember("uploaded"); 205 | auto progress = peer.FindMember("progress"); 206 | if ( 207 | size > 0 && 208 | uploaded != peer.MemberEnd() && uploaded->value.IsUint64() && 209 | progress != peer.MemberEnd() && progress->value.IsNumber() 210 | ) { 211 | int should_progress = uploaded->value.GetUint64() * 100 / size; 212 | int actrue_progress = progress->value.GetDouble() * 100; 213 | if (should_progress - actrue_progress > 2) { 214 | banned_list[ip_address] = expire; 215 | notChangedFlag = false; 216 | continue; 217 | } 218 | } 219 | } 220 | } 221 | 222 | // 设置封禁列表 223 | void set_ban_list() { 224 | CURL *curl = curl_easy_init(); 225 | 226 | if (!curl) { 227 | throw "update_peers: CURL init failure!"; 228 | } 229 | 230 | std::string banned; 231 | 232 | std::string data = "json=%7B%22banned_IPs%22%3A%22"; 233 | for (const auto &m : banned_list) { 234 | data.append(std::regex_replace(m.first, regCOLON, "%3A")).append("%5Cn"); 235 | banned += m.first + " "; 236 | } 237 | data.append("%22%7D"); 238 | 239 | struct curl_slist *chunk = NULL; 240 | chunk = curl_slist_append(chunk, "Content-Type: application/x-www-form-urlencoded"); 241 | 242 | curl_easy_setopt(curl, CURLOPT_URL, (std::string(HOST) + "/api/v2/app/setPreferences").c_str()); 243 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); 244 | curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); 245 | curl_easy_setopt(curl, CURLOPT_COOKIEFILE, COOKIEFILE); 246 | curl_easy_setopt(curl, CURLOPT_COOKIEJAR, COOKIEFILE); 247 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); 248 | // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1l); 249 | 250 | CURLcode res = curl_easy_perform(curl); 251 | 252 | if (res != CURLE_OK) { 253 | throw std::string("set_ban_list: CURL request failure! ") + curl_easy_strerror(res); 254 | } else { 255 | notChangedFlag = true; 256 | std::cout << "[I] Banned: " << banned << std::endl; 257 | } 258 | 259 | curl_easy_cleanup(curl); 260 | curl_slist_free_all(chunk); 261 | } 262 | 263 | // CURL 读取数据处理函数 264 | size_t CURL_write_stdString(void *contents, size_t size, size_t nmemb, std::string *str) { 265 | size_t newLength = size * nmemb; 266 | try { 267 | str->append((char*)contents, newLength); 268 | } catch(std::bad_alloc &e) { 269 | //handle memory problem 270 | return 0; 271 | } 272 | return newLength; 273 | } 274 | --------------------------------------------------------------------------------