blob: 93355f9a6418260266fff6cdf461280667fa4c5d [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;
root59a69f82025-06-05 08:35:22 +000025
26 public DataCaptureProxy(String trackerHost, int trackerPort) {
27 this.trackerHost = trackerHost;
28 this.trackerPort = trackerPort;
TRM-coding87c24972025-06-07 14:05:29 +080029 this.tracker = new Tracker(); // 初始化 Tracker 实例
root59a69f82025-06-05 08:35:22 +000030 }
31
32 @Override
33 public void handle(Request req, Response resp) {
34 try {
35 // 提取并打印关键参数
TRM-coding83df9e22025-06-09 17:51:05 +080036 String infoHashParam = req.getParameter("info_hash");
37 String infoHash = null;
38 String infoHashHex = null;
39
40 // 尝试从原始查询字符串中直接提取 info_hash
41 String rawQuery = req.getQuery().toString();
42 System.out.println("DEBUG: Raw query string: " + rawQuery);
43
44 // 正确处理 info_hash 的字符集
45 if (infoHashParam != null) {
46 try {
47 // 先尝试从原始查询字符串中提取 info_hash
48 byte[] infoHashBytes = extractInfoHashFromRawQuery(rawQuery);
49
50 if (infoHashBytes == null || infoHashBytes.length != 20) {
51 System.out.println("DEBUG: Raw query extraction failed, trying parameter method");
52 // 回退到参数解析方法
53 infoHashBytes = processInfoHashParameter(infoHashParam);
54 }
55
56 if (infoHashBytes != null) {
57 // 转换为十六进制字符串
58 StringBuilder hexBuilder = new StringBuilder();
59 for (byte b : infoHashBytes) {
60 hexBuilder.append(String.format("%02x", b & 0xFF));
61 }
62 infoHashHex = hexBuilder.toString();
63
64 // 调试输出
65 System.out.print("DEBUG: Final byte values (hex): ");
66 for (byte b : infoHashBytes) {
67 System.out.printf("%02x ", b & 0xFF);
68 }
69 System.out.println();
70
71 System.out.println("DEBUG: Final infoHashHex: " + infoHashHex);
72 System.out.println("DEBUG: Final infoHashHex length: " + infoHashHex.length());
73
74 // 验证最终哈希长度
75 if (infoHashHex.length() != 40) {
76 System.err.println("ERROR: Final info_hash hex should be 40 characters, but got " + infoHashHex.length());
77 }
78 }
79
80 } catch (Exception e) {
81 System.err.println("Error processing info_hash: " + e.getMessage());
82 e.printStackTrace();
83 infoHash = infoHashParam; // 回退到原始值
84 infoHashHex = "invalid_hash";
85 }
86 }
87
88 // ===== 新增:容错匹配,替换为数据库中最相似的完整 hash =====
89 if (infoHashHex != null && !infoHashHex.isEmpty()) {
90 List<String> allHashes = tracker.getAllInfoHashes(); // 从 DB 拉取所有 hash
91 String best = findBestMatchingInfoHash(infoHashHex, allHashes);
92 if (best != null) {
93 System.out.println("DEBUG: Fallback matched infoHash: " + best);
94 infoHashHex = best;
95 }
96 }
97
98 // 提取其他参数
TRM-coding87c24972025-06-07 14:05:29 +080099 String uploaded = req.getParameter("uploaded");
100 String downloaded = req.getParameter("downloaded");
101 String passkey = req.getParameter("passkey");
TRM-codingd5de51e2025-06-08 03:27:01 +0800102 String port = req.getParameter("port"); // qBittorrent 服务端端口
TRM-coding87c24972025-06-07 14:05:29 +0800103
TRM-codingd5de51e2025-06-08 03:27:01 +0800104 // 获取客户端IP地址和端口
TRM-coding87c24972025-06-07 14:05:29 +0800105 String clientIp;
TRM-codingd5de51e2025-06-08 03:27:01 +0800106 int clientPort = -1;
TRM-coding87c24972025-06-07 14:05:29 +0800107 // 直接从 TCP 连接(socket 源地址)中读取
108 SocketAddress socketAddress = req.getClientAddress();
109 if (socketAddress instanceof InetSocketAddress) {
TRM-codingd5de51e2025-06-08 03:27:01 +0800110 InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
111 clientIp = inetSocketAddress.getAddress().getHostAddress();
112 clientPort = inetSocketAddress.getPort();
TRM-coding87c24972025-06-07 14:05:29 +0800113 } else {
114 // 兜底写法,将整个 SocketAddress 转为字符串
115 clientIp = socketAddress.toString();
116 }
117
root59a69f82025-06-05 08:35:22 +0000118 System.out.println(
TRM-coding83df9e22025-06-09 17:51:05 +0800119 "Captured announce → info_hash_hex=" + infoHashHex +
root59a69f82025-06-05 08:35:22 +0000120 ", uploaded=" + uploaded +
121 ", downloaded=" + downloaded +
TRM-coding87c24972025-06-07 14:05:29 +0800122 ", passkey=" + passkey +
TRM-codingd5de51e2025-06-08 03:27:01 +0800123 ", client_ip=" + clientIp +
124 ", client_port=" + clientPort +
125 ", qbt_service_port=" + port
root59a69f82025-06-05 08:35:22 +0000126 );
127
TRM-coding83df9e22025-06-09 17:51:05 +0800128 // 调用 Tracker 方法更新上传和下载数据(使用校正后的 infoHashHex)
129 if (passkey != null && !passkey.isEmpty() && infoHashHex != null && !infoHashHex.isEmpty()) {
TRM-codingcdfe5482025-06-06 17:31:01 +0800130 try {
131 if (uploaded != null && !uploaded.isEmpty()) {
132 int uploadValue = Integer.parseInt(uploaded);
133 if (uploadValue > 0) {
TRM-coding508b31f2025-06-09 02:07:14 +0800134 try {
TRM-coding83df9e22025-06-09 17:51:05 +0800135 tracker.AddUpLoad(passkey, uploadValue, infoHashHex);
TRM-coding508b31f2025-06-09 02:07:14 +0800136 } catch (javax.persistence.NoResultException e) {
TRM-coding83df9e22025-06-09 17:51:05 +0800137 System.out.println("Skipping upload update: info_hash not found in database - " + infoHashHex);
TRM-coding508b31f2025-06-09 02:07:14 +0800138 }
TRM-codingcdfe5482025-06-06 17:31:01 +0800139 }
140 }
141
142 if (downloaded != null && !downloaded.isEmpty()) {
143 int downloadValue = Integer.parseInt(downloaded);
144 if (downloadValue > 0) {
TRM-coding508b31f2025-06-09 02:07:14 +0800145 try {
TRM-coding83df9e22025-06-09 17:51:05 +0800146 tracker.AddDownload(passkey, downloadValue, 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 download 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 } catch (NumberFormatException e) {
153 System.err.println("Error parsing upload/download values: " + e.getMessage());
154 }
155 }
156
root59a69f82025-06-05 08:35:22 +0000157 // 构造转发 URL
TRM-coding87c24972025-06-07 14:05:29 +0800158 String path = req.getPath().getPath();
root59a69f82025-06-05 08:35:22 +0000159 String query = req.getQuery().toString();
160 String targetUrl = "http://" + trackerHost + ":" + trackerPort
161 + path + "?" + query;
162
163 HttpURLConnection connection =
164 (HttpURLConnection) new URL(targetUrl).openConnection();
165 connection.setRequestMethod("GET");
166
167 // 转发响应码和类型
168 resp.setCode(connection.getResponseCode());
169 String ct = connection.getContentType();
170 if (ct != null) resp.setValue("Content-Type", ct);
171
172 // 转发响应体
173 try (InputStream in = connection.getInputStream();
174 OutputStream out = resp.getOutputStream()) {
175 byte[] buf = new byte[8192];
176 int len;
177 while ((len = in.read(buf)) != -1) {
178 out.write(buf, 0, len);
179 }
180 }
181
182 } catch (Exception e) {
183 try {
184 resp.setCode(500);
185 resp.close();
186 } catch (Exception ignore) {}
187 e.printStackTrace();
188 }
189 }
TRM-coding83df9e22025-06-09 17:51:05 +0800190
191 /**
192 * 从原始查询字符串中提取 info_hash 的字节
193 */
194 private byte[] extractInfoHashFromRawQuery(String rawQuery) {
195 try {
196 // 查找 info_hash= 的位置
197 int start = rawQuery.indexOf("info_hash=");
198 if (start == -1) {
199 return null;
200 }
201
202 start += "info_hash=".length(); // 跳过 "info_hash="
203
204 // 查找下一个 & 或字符串结尾
205 int end = rawQuery.indexOf('&', start);
206 if (end == -1) {
207 end = rawQuery.length();
208 }
209
210 String encodedInfoHash = rawQuery.substring(start, end);
211 System.out.println("DEBUG: Extracted encoded info_hash: " + encodedInfoHash);
212 System.out.println("DEBUG: Encoded info_hash length: " + encodedInfoHash.length());
213
214 // 手动解码 percent-encoding,确保正确处理二进制数据
215 byte[] bytes = decodePercentEncoding(encodedInfoHash);
216 System.out.println("DEBUG: Raw extraction - bytes length: " + bytes.length);
217
218 return bytes.length == 20 ? bytes : null;
219
220 } catch (Exception e) {
221 System.err.println("Error extracting info_hash from raw query: " + e.getMessage());
222 return null;
223 }
224 }
225
226
227
228 /**
229 * 手动解码 percent-encoding,确保正确处理二进制数据
230 */
231 private byte[] decodePercentEncoding(String encoded) {
232 try {
233 int length = encoded.length();
234 byte[] result = new byte[length]; // 最大可能长度
235 int resultIndex = 0;
236
237 for (int i = 0; i < length; i++) {
238 char c = encoded.charAt(i);
239
240 if (c == '%' && i + 2 < length) {
241 // 解码 %XX
242 String hex = encoded.substring(i + 1, i + 3);
243 try {
244 int value = Integer.parseInt(hex, 16);
245 result[resultIndex++] = (byte) value;
246 i += 2; // 跳过接下来的两个字符
247 } catch (NumberFormatException e) {
248 // 如果不是有效的十六进制,当作普通字符处理
249 result[resultIndex++] = (byte) c;
250 }
251 } else if (c == '+') {
252 // '+' 在 URL 编码中表示空格
253 result[resultIndex++] = (byte) ' ';
254 } else {
255 // 普通字符
256 result[resultIndex++] = (byte) c;
257 }
258 }
259
260 // 创建正确长度的数组
261 byte[] finalResult = new byte[resultIndex];
262 System.arraycopy(result, 0, finalResult, 0, resultIndex);
263
264 System.out.println("DEBUG: Percent decoding - input length: " + length + ", output length: " + resultIndex);
265
266 return finalResult;
267
268 } catch (Exception e) {
269 System.err.println("Error in decodePercentEncoding: " + e.getMessage());
270 return new byte[0];
271 }
272 }
273
274 /**
275 * 处理通过参数解析得到的 info_hash
276 */
277 private byte[] processInfoHashParameter(String infoHashParam) {
278 try {
279 // 先尝试手动 percent-encoding 解码
280 byte[] infoHashBytes = decodePercentEncoding(infoHashParam);
281
282 // 调试信息
283 System.out.println("DEBUG: Parameter method - Original: " + infoHashParam);
284 System.out.println("DEBUG: Parameter method - Original length: " + infoHashParam.length());
285 System.out.println("DEBUG: Parameter method - Manual decode bytes length: " + infoHashBytes.length);
286
287 // 如果手动解码失败,尝试标准方法
288 if (infoHashBytes.length != 20) {
289 System.out.println("DEBUG: Manual decode failed, trying URLDecoder with ISO-8859-1");
290 try {
291 // 使用 ISO-8859-1 而不是 UTF-8
292 String decodedParam = URLDecoder.decode(infoHashParam, StandardCharsets.ISO_8859_1.name());
293 infoHashBytes = decodedParam.getBytes(StandardCharsets.ISO_8859_1);
294 System.out.println("DEBUG: URLDecoder ISO-8859-1 result length: " + infoHashBytes.length);
295 } catch (Exception e2) {
296 System.err.println("URLDecoder with ISO-8859-1 failed: " + e2.getMessage());
297 }
298 }
299
300 // 最后尝试:直接将字符串转为字节
301 if (infoHashBytes.length != 20) {
302 System.out.println("DEBUG: Trying direct byte conversion");
303 infoHashBytes = infoHashParam.getBytes(StandardCharsets.ISO_8859_1);
304 System.out.println("DEBUG: Direct conversion result length: " + infoHashBytes.length);
305 }
306
307 // 验证字节长度
308 if (infoHashBytes.length != 20) {
309 System.err.println("WARNING: info_hash should be 20 bytes, but got " + infoHashBytes.length + " bytes");
310 }
311
312 return infoHashBytes;
313
314 } catch (Exception e) {
315 System.err.println("Error in processInfoHashParameter: " + e.getMessage());
316 return null;
317 }
318 }
319
320 /**
321 * 在候选 hash 列表中,找到与 targetHex 最长公共子序列(LCS)最大的那个
322 */
323 private String findBestMatchingInfoHash(String targetHex, List<String> candidates) {
324 String best = null;
325 int bestLen = -1;
326 for (String cand : candidates) {
327 int len = longestCommonSubseq(targetHex, cand);
328 if (len > bestLen) {
329 bestLen = len;
330 best = cand;
331 }
332 }
333 return best;
334 }
335
336 // 计算两字符串的 LCS 长度
337 private int longestCommonSubseq(String a, String b) {
338 int n = a.length(), m = b.length();
339 int[] dp = new int[m+1];
340 for (int i = 1; i <= n; i++) {
341 int prev = 0;
342 for (int j = 1; j <= m; j++) {
343 int temp = dp[j];
344 if (a.charAt(i-1) == b.charAt(j-1)) {
345 dp[j] = prev + 1;
346 } else {
347 dp[j] = Math.max(dp[j], dp[j-1]);
348 }
349 prev = temp;
350 }
351 }
352 return dp[m];
353 }
root59a69f82025-06-05 08:35:22 +0000354}