blob: 9b113495f43bd34dea6f9c9434969bf9cc21db97 [file] [log] [blame]
root59a69f82025-06-05 08:35:22 +00001package tracker;
2
3import java.io.InputStream;
4import java.io.OutputStream;
5import java.net.HttpURLConnection;
TRM-coding87c24972025-06-07 14:05:29 +08006import java.net.InetSocketAddress;
7import java.net.SocketAddress;
root59a69f82025-06-05 08:35:22 +00008import java.net.URL;
TRM-coding83df9e22025-06-09 17:51:05 +08009import java.net.URLDecoder;
10import java.nio.charset.StandardCharsets;
11import java.util.List;
TRM-codingd5de51e2025-06-08 03:27:01 +080012import tracker.Tracker;
root59a69f82025-06-05 08:35:22 +000013import org.simpleframework.http.Request;
14import org.simpleframework.http.Response;
15import org.simpleframework.http.core.Container;
16
17/**
18 * 拦截 announce 请求,打印参数后转发给真实 Tracker。
19 */
20public class DataCaptureProxy implements Container {
21
22 private final String trackerHost;
23 private final int trackerPort;
TRM-codingcdfe5482025-06-06 17:31:01 +080024 private final Tracker tracker;
TRM-coding24b60182025-06-09 21:19:45 +080025 private final CheatDetectionScheduler cheatScheduler; // 新增
root59a69f82025-06-05 08:35:22 +000026
27 public DataCaptureProxy(String trackerHost, int trackerPort) {
28 this.trackerHost = trackerHost;
29 this.trackerPort = trackerPort;
TRM-coding24b60182025-06-09 21:19:45 +080030 this.tracker = new Tracker();
31
32 // 初始化并启动作弊检测调度器
33 this.cheatScheduler = new CheatDetectionScheduler();
34 this.cheatScheduler.start();
35
36 // 添加JVM关闭钩子,确保优雅关闭
37 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
38 System.out.println("Shutting down CheatDetectionScheduler...");
39 this.cheatScheduler.stop();
40 }));
root59a69f82025-06-05 08:35:22 +000041 }
42
43 @Override
44 public void handle(Request req, Response resp) {
45 try {
46 // 提取并打印关键参数
TRM-coding83df9e22025-06-09 17:51:05 +080047 String infoHashParam = req.getParameter("info_hash");
48 String infoHash = null;
49 String infoHashHex = null;
50
51 // 尝试从原始查询字符串中直接提取 info_hash
52 String rawQuery = req.getQuery().toString();
53 System.out.println("DEBUG: Raw query string: " + rawQuery);
54
55 // 正确处理 info_hash 的字符集
56 if (infoHashParam != null) {
57 try {
58 // 先尝试从原始查询字符串中提取 info_hash
59 byte[] infoHashBytes = extractInfoHashFromRawQuery(rawQuery);
60
61 if (infoHashBytes == null || infoHashBytes.length != 20) {
62 System.out.println("DEBUG: Raw query extraction failed, trying parameter method");
63 // 回退到参数解析方法
64 infoHashBytes = processInfoHashParameter(infoHashParam);
65 }
66
67 if (infoHashBytes != null) {
68 // 转换为十六进制字符串
69 StringBuilder hexBuilder = new StringBuilder();
70 for (byte b : infoHashBytes) {
71 hexBuilder.append(String.format("%02x", b & 0xFF));
72 }
73 infoHashHex = hexBuilder.toString();
74
75 // 调试输出
76 System.out.print("DEBUG: Final byte values (hex): ");
77 for (byte b : infoHashBytes) {
78 System.out.printf("%02x ", b & 0xFF);
79 }
80 System.out.println();
81
82 System.out.println("DEBUG: Final infoHashHex: " + infoHashHex);
83 System.out.println("DEBUG: Final infoHashHex length: " + infoHashHex.length());
84
85 // 验证最终哈希长度
86 if (infoHashHex.length() != 40) {
87 System.err.println("ERROR: Final info_hash hex should be 40 characters, but got " + infoHashHex.length());
88 }
89 }
90
91 } catch (Exception e) {
92 System.err.println("Error processing info_hash: " + e.getMessage());
93 e.printStackTrace();
94 infoHash = infoHashParam; // 回退到原始值
95 infoHashHex = "invalid_hash";
96 }
97 }
98
99 // ===== 新增:容错匹配,替换为数据库中最相似的完整 hash =====
100 if (infoHashHex != null && !infoHashHex.isEmpty()) {
101 List<String> allHashes = tracker.getAllInfoHashes(); // 从 DB 拉取所有 hash
102 String best = findBestMatchingInfoHash(infoHashHex, allHashes);
103 if (best != null) {
104 System.out.println("DEBUG: Fallback matched infoHash: " + best);
105 infoHashHex = best;
106 }
107 }
108
109 // 提取其他参数
TRM-coding87c24972025-06-07 14:05:29 +0800110 String uploaded = req.getParameter("uploaded");
111 String downloaded = req.getParameter("downloaded");
112 String passkey = req.getParameter("passkey");
TRM-codingd5de51e2025-06-08 03:27:01 +0800113 String port = req.getParameter("port"); // qBittorrent 服务端端口
TRM-coding87c24972025-06-07 14:05:29 +0800114
TRM-codingd5de51e2025-06-08 03:27:01 +0800115 // 获取客户端IP地址和端口
TRM-coding87c24972025-06-07 14:05:29 +0800116 String clientIp;
TRM-codingd5de51e2025-06-08 03:27:01 +0800117 int clientPort = -1;
TRM-coding87c24972025-06-07 14:05:29 +0800118 // 直接从 TCP 连接(socket 源地址)中读取
119 SocketAddress socketAddress = req.getClientAddress();
120 if (socketAddress instanceof InetSocketAddress) {
TRM-codingd5de51e2025-06-08 03:27:01 +0800121 InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
122 clientIp = inetSocketAddress.getAddress().getHostAddress();
123 clientPort = inetSocketAddress.getPort();
TRM-coding87c24972025-06-07 14:05:29 +0800124 } else {
125 // 兜底写法,将整个 SocketAddress 转为字符串
126 clientIp = socketAddress.toString();
127 }
128
root59a69f82025-06-05 08:35:22 +0000129 System.out.println(
TRM-coding83df9e22025-06-09 17:51:05 +0800130 "Captured announce → info_hash_hex=" + infoHashHex +
root59a69f82025-06-05 08:35:22 +0000131 ", uploaded=" + uploaded +
132 ", downloaded=" + downloaded +
TRM-coding87c24972025-06-07 14:05:29 +0800133 ", passkey=" + passkey +
TRM-codingd5de51e2025-06-08 03:27:01 +0800134 ", client_ip=" + clientIp +
135 ", client_port=" + clientPort +
136 ", qbt_service_port=" + port
root59a69f82025-06-05 08:35:22 +0000137 );
138
TRM-coding83df9e22025-06-09 17:51:05 +0800139 // 调用 Tracker 方法更新上传和下载数据(使用校正后的 infoHashHex)
140 if (passkey != null && !passkey.isEmpty() && infoHashHex != null && !infoHashHex.isEmpty()) {
TRM-codingcdfe5482025-06-06 17:31:01 +0800141 try {
142 if (uploaded != null && !uploaded.isEmpty()) {
143 int uploadValue = Integer.parseInt(uploaded);
144 if (uploadValue > 0) {
TRM-coding508b31f2025-06-09 02:07:14 +0800145 try {
TRM-coding83df9e22025-06-09 17:51:05 +0800146 tracker.AddUpLoad(passkey, uploadValue, infoHashHex);
TRM-coding508b31f2025-06-09 02:07:14 +0800147 } catch (javax.persistence.NoResultException e) {
TRM-coding83df9e22025-06-09 17:51:05 +0800148 System.out.println("Skipping upload update: info_hash not found in database - " + infoHashHex);
TRM-coding508b31f2025-06-09 02:07:14 +0800149 }
TRM-codingcdfe5482025-06-06 17:31:01 +0800150 }
151 }
152
153 if (downloaded != null && !downloaded.isEmpty()) {
154 int downloadValue = Integer.parseInt(downloaded);
155 if (downloadValue > 0) {
TRM-coding508b31f2025-06-09 02:07:14 +0800156 try {
TRM-coding83df9e22025-06-09 17:51:05 +0800157 tracker.AddDownload(passkey, downloadValue, infoHashHex);
TRM-coding508b31f2025-06-09 02:07:14 +0800158 } catch (javax.persistence.NoResultException e) {
TRM-coding83df9e22025-06-09 17:51:05 +0800159 System.out.println("Skipping download update: info_hash not found in database - " + infoHashHex);
TRM-coding508b31f2025-06-09 02:07:14 +0800160 }
TRM-codingcdfe5482025-06-06 17:31:01 +0800161 }
162 }
163 } catch (NumberFormatException e) {
164 System.err.println("Error parsing upload/download values: " + e.getMessage());
165 }
166 }
167
root59a69f82025-06-05 08:35:22 +0000168 // 构造转发 URL
TRM-coding87c24972025-06-07 14:05:29 +0800169 String path = req.getPath().getPath();
root59a69f82025-06-05 08:35:22 +0000170 String query = req.getQuery().toString();
171 String targetUrl = "http://" + trackerHost + ":" + trackerPort
172 + path + "?" + query;
173
174 HttpURLConnection connection =
175 (HttpURLConnection) new URL(targetUrl).openConnection();
176 connection.setRequestMethod("GET");
177
178 // 转发响应码和类型
179 resp.setCode(connection.getResponseCode());
180 String ct = connection.getContentType();
181 if (ct != null) resp.setValue("Content-Type", ct);
182
183 // 转发响应体
184 try (InputStream in = connection.getInputStream();
185 OutputStream out = resp.getOutputStream()) {
186 byte[] buf = new byte[8192];
187 int len;
188 while ((len = in.read(buf)) != -1) {
189 out.write(buf, 0, len);
190 }
191 }
192
193 } catch (Exception e) {
194 try {
195 resp.setCode(500);
196 resp.close();
197 } catch (Exception ignore) {}
198 e.printStackTrace();
199 }
200 }
TRM-coding83df9e22025-06-09 17:51:05 +0800201
202 /**
203 * 从原始查询字符串中提取 info_hash 的字节
204 */
205 private byte[] extractInfoHashFromRawQuery(String rawQuery) {
206 try {
207 // 查找 info_hash= 的位置
208 int start = rawQuery.indexOf("info_hash=");
209 if (start == -1) {
210 return null;
211 }
212
213 start += "info_hash=".length(); // 跳过 "info_hash="
214
215 // 查找下一个 & 或字符串结尾
216 int end = rawQuery.indexOf('&', start);
217 if (end == -1) {
218 end = rawQuery.length();
219 }
220
221 String encodedInfoHash = rawQuery.substring(start, end);
222 System.out.println("DEBUG: Extracted encoded info_hash: " + encodedInfoHash);
223 System.out.println("DEBUG: Encoded info_hash length: " + encodedInfoHash.length());
224
225 // 手动解码 percent-encoding,确保正确处理二进制数据
226 byte[] bytes = decodePercentEncoding(encodedInfoHash);
227 System.out.println("DEBUG: Raw extraction - bytes length: " + bytes.length);
228
229 return bytes.length == 20 ? bytes : null;
230
231 } catch (Exception e) {
232 System.err.println("Error extracting info_hash from raw query: " + e.getMessage());
233 return null;
234 }
235 }
236
237
238
239 /**
240 * 手动解码 percent-encoding,确保正确处理二进制数据
241 */
242 private byte[] decodePercentEncoding(String encoded) {
243 try {
244 int length = encoded.length();
245 byte[] result = new byte[length]; // 最大可能长度
246 int resultIndex = 0;
247
248 for (int i = 0; i < length; i++) {
249 char c = encoded.charAt(i);
250
251 if (c == '%' && i + 2 < length) {
252 // 解码 %XX
253 String hex = encoded.substring(i + 1, i + 3);
254 try {
255 int value = Integer.parseInt(hex, 16);
256 result[resultIndex++] = (byte) value;
257 i += 2; // 跳过接下来的两个字符
258 } catch (NumberFormatException e) {
259 // 如果不是有效的十六进制,当作普通字符处理
260 result[resultIndex++] = (byte) c;
261 }
262 } else if (c == '+') {
263 // '+' 在 URL 编码中表示空格
264 result[resultIndex++] = (byte) ' ';
265 } else {
266 // 普通字符
267 result[resultIndex++] = (byte) c;
268 }
269 }
270
271 // 创建正确长度的数组
272 byte[] finalResult = new byte[resultIndex];
273 System.arraycopy(result, 0, finalResult, 0, resultIndex);
274
275 System.out.println("DEBUG: Percent decoding - input length: " + length + ", output length: " + resultIndex);
276
277 return finalResult;
278
279 } catch (Exception e) {
280 System.err.println("Error in decodePercentEncoding: " + e.getMessage());
281 return new byte[0];
282 }
283 }
284
285 /**
286 * 处理通过参数解析得到的 info_hash
287 */
288 private byte[] processInfoHashParameter(String infoHashParam) {
289 try {
290 // 先尝试手动 percent-encoding 解码
291 byte[] infoHashBytes = decodePercentEncoding(infoHashParam);
292
293 // 调试信息
294 System.out.println("DEBUG: Parameter method - Original: " + infoHashParam);
295 System.out.println("DEBUG: Parameter method - Original length: " + infoHashParam.length());
296 System.out.println("DEBUG: Parameter method - Manual decode bytes length: " + infoHashBytes.length);
297
298 // 如果手动解码失败,尝试标准方法
299 if (infoHashBytes.length != 20) {
300 System.out.println("DEBUG: Manual decode failed, trying URLDecoder with ISO-8859-1");
301 try {
302 // 使用 ISO-8859-1 而不是 UTF-8
303 String decodedParam = URLDecoder.decode(infoHashParam, StandardCharsets.ISO_8859_1.name());
304 infoHashBytes = decodedParam.getBytes(StandardCharsets.ISO_8859_1);
305 System.out.println("DEBUG: URLDecoder ISO-8859-1 result length: " + infoHashBytes.length);
306 } catch (Exception e2) {
307 System.err.println("URLDecoder with ISO-8859-1 failed: " + e2.getMessage());
308 }
309 }
310
311 // 最后尝试:直接将字符串转为字节
312 if (infoHashBytes.length != 20) {
313 System.out.println("DEBUG: Trying direct byte conversion");
314 infoHashBytes = infoHashParam.getBytes(StandardCharsets.ISO_8859_1);
315 System.out.println("DEBUG: Direct conversion result length: " + infoHashBytes.length);
316 }
317
318 // 验证字节长度
319 if (infoHashBytes.length != 20) {
320 System.err.println("WARNING: info_hash should be 20 bytes, but got " + infoHashBytes.length + " bytes");
321 }
322
323 return infoHashBytes;
324
325 } catch (Exception e) {
326 System.err.println("Error in processInfoHashParameter: " + e.getMessage());
327 return null;
328 }
329 }
330
331 /**
332 * 在候选 hash 列表中,找到与 targetHex 最长公共子序列(LCS)最大的那个
333 */
334 private String findBestMatchingInfoHash(String targetHex, List<String> candidates) {
335 String best = null;
336 int bestLen = -1;
337 for (String cand : candidates) {
338 int len = longestCommonSubseq(targetHex, cand);
339 if (len > bestLen) {
340 bestLen = len;
341 best = cand;
342 }
343 }
344 return best;
345 }
346
347 // 计算两字符串的 LCS 长度
348 private int longestCommonSubseq(String a, String b) {
349 int n = a.length(), m = b.length();
350 int[] dp = new int[m+1];
351 for (int i = 1; i <= n; i++) {
352 int prev = 0;
353 for (int j = 1; j <= m; j++) {
354 int temp = dp[j];
355 if (a.charAt(i-1) == b.charAt(j-1)) {
356 dp[j] = prev + 1;
357 } else {
358 dp[j] = Math.max(dp[j], dp[j-1]);
359 }
360 prev = temp;
361 }
362 }
363 return dp[m];
364 }
TRM-coding24b60182025-06-09 21:19:45 +0800365
366 /**
367 * 获取调度器状态
368 */
369 public boolean isCheatDetectionRunning() {
370 return cheatScheduler.isRunning();
371 }
372
373 /**
374 * 手动停止调度器(如果需要的话)
375 */
376 public void stopCheatDetection() {
377 cheatScheduler.stop();
378 }
root59a69f82025-06-05 08:35:22 +0000379}