Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 1 | package com.pt.utils; |
| 2 | |
| 3 | import java.io.*; |
| 4 | import java.util.*; |
| 5 | |
| 6 | public class TorrentPasskeyModifier { |
| 7 | |
| 8 | private byte[] infoValueBytes; // 保存 info 的 value 原始 bencode |
| 9 | |
| 10 | public byte[] analyzeTorrentFile(byte[] fileBytes, String username) throws IOException { |
| 11 | ByteArrayInputStream in = new ByteArrayInputStream(fileBytes); |
| 12 | Map<String, Object> torrentMap = decodeWithInfoPreservation(in); |
| 13 | |
| 14 | // 修改 announce |
| 15 | if (torrentMap.containsKey("announce")) { |
| 16 | String announce = (String) torrentMap.get("announce"); |
| 17 | torrentMap.put("announce", replacePasskeyInUrl(announce, username)); |
| 18 | } |
| 19 | |
| 20 | // 修改 announce-list |
| 21 | if (torrentMap.containsKey("announce-list")) { |
| 22 | Object list = torrentMap.get("announce-list"); |
| 23 | if (list instanceof List) { |
| 24 | replacePasskeyInAnnounceList((List<?>) list, username); |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // 编码为新 torrent 文件 |
| 29 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| 30 | out.write('d'); |
| 31 | |
| 32 | List<String> keys = new ArrayList<>(torrentMap.keySet()); |
| 33 | Collections.sort(keys); |
| 34 | for (String key : keys) { |
| 35 | encodeString(key, out); |
| 36 | if ("info".equals(key)) { |
| 37 | out.write(infoValueBytes); // 写入原始 info 的 value bencode |
| 38 | } else { |
| 39 | encode(torrentMap.get(key), out); |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | out.write('e'); |
| 44 | return out.toByteArray(); |
| 45 | } |
| 46 | |
| 47 | private String replacePasskeyInUrl(String url, String username) { |
| 48 | if (url == null) return null; |
| 49 | if (url.contains("passkey=")) { |
| 50 | return url.replaceAll("(?<=passkey=)[^&]*", username); |
| 51 | } else { |
| 52 | return url.contains("?") |
| 53 | ? url + "&passkey=" + username |
| 54 | : url + "?passkey=" + username; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | private void replacePasskeyInAnnounceList(List<?> list, String username) { |
| 59 | for (Object tierObj : list) { |
| 60 | if (tierObj instanceof List) { |
| 61 | List<Object> tier = (List<Object>) tierObj; |
| 62 | for (int i = 0; i < tier.size(); i++) { |
| 63 | Object url = tier.get(i); |
| 64 | if (url instanceof String) { |
| 65 | tier.set(i, replacePasskeyInUrl((String) url, username)); |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // --- Bencode 解码并提取原始 info 字典字节 --- |
| 73 | private Map<String, Object> decodeWithInfoPreservation(InputStream in) throws IOException { |
| 74 | if (in.read() != 'd') throw new IOException("Not a bencode dict"); |
| 75 | Map<String, Object> map = new LinkedHashMap<>(); |
| 76 | while (true) { |
| 77 | int c = in.read(); |
| 78 | if (c == 'e') break; |
| 79 | if (c == -1) throw new EOFException(); |
| 80 | |
| 81 | String key = decodeString(in, (char) c); |
| 82 | if ("info".equals(key)) { |
| 83 | ByteArrayOutputStream infoOut = new ByteArrayOutputStream(); |
| 84 | in.mark(1); |
| 85 | int b = in.read(); |
| 86 | if (b != 'd') throw new IOException("Invalid info dict"); |
| 87 | infoOut.write(b); |
| 88 | int depth = 1; |
| 89 | |
| 90 | while (depth > 0) { |
| 91 | b = in.read(); |
| 92 | if (b == -1) throw new IOException("Unexpected EOF in info"); |
| 93 | infoOut.write(b); |
| 94 | |
| 95 | if (b == 'd' || b == 'l') depth++; |
| 96 | else if (b == 'e') depth--; |
| 97 | else if (b >= '0' && b <= '9') { |
| 98 | int len = b - '0'; |
| 99 | while (true) { |
| 100 | int nc = in.read(); |
| 101 | infoOut.write(nc); |
| 102 | if (nc == ':') break; |
| 103 | len = len * 10 + (nc - '0'); |
| 104 | } |
| 105 | for (int i = 0; i < len; i++) { |
| 106 | infoOut.write(in.read()); |
| 107 | } |
| 108 | } else if (b == 'i') { |
| 109 | while (true) { |
| 110 | int nc = in.read(); |
| 111 | infoOut.write(nc); |
| 112 | if (nc == 'e') break; |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | this.infoValueBytes = infoOut.toByteArray(); |
| 118 | map.put("info", null); // 占位 |
| 119 | } else { |
| 120 | map.put(key, decode(in)); |
| 121 | } |
| 122 | } |
| 123 | return map; |
| 124 | } |
| 125 | |
| 126 | private Object decode(InputStream in) throws IOException { |
| 127 | int c = in.read(); |
| 128 | if (c == -1) throw new EOFException(); |
| 129 | if (c == 'd') return decodeDict(in); |
| 130 | if (c == 'l') return decodeList(in); |
| 131 | if (c == 'i') return decodeInt(in); |
| 132 | if (c >= '0' && c <= '9') return decodeString(in, (char) c); |
| 133 | throw new IOException("Invalid bencode start: " + (char) c); |
| 134 | } |
| 135 | |
| 136 | private Map<String, Object> decodeDict(InputStream in) throws IOException { |
| 137 | Map<String, Object> map = new LinkedHashMap<>(); |
| 138 | while (true) { |
| 139 | int c = in.read(); |
| 140 | if (c == 'e') break; |
| 141 | if (c == -1) throw new EOFException(); |
| 142 | String key = decodeString(in, (char) c); |
| 143 | map.put(key, decode(in)); |
| 144 | } |
| 145 | return map; |
| 146 | } |
| 147 | |
| 148 | private List<Object> decodeList(InputStream in) throws IOException { |
| 149 | List<Object> list = new ArrayList<>(); |
| 150 | while (true) { |
| 151 | int c = in.read(); |
| 152 | if (c == 'e') break; |
| 153 | if (c == -1) throw new EOFException(); |
| 154 | in.reset(); |
| 155 | list.add(decode(in)); |
| 156 | } |
| 157 | return list; |
| 158 | } |
| 159 | |
| 160 | private String decodeString(InputStream in, char firstDigit) throws IOException { |
| 161 | StringBuilder sb = new StringBuilder(); |
| 162 | sb.append(firstDigit); |
| 163 | while (true) { |
| 164 | int c = in.read(); |
| 165 | if (c == ':') break; |
| 166 | sb.append((char) c); |
| 167 | } |
| 168 | int len = Integer.parseInt(sb.toString()); |
| 169 | byte[] buf = new byte[len]; |
| 170 | if (in.read(buf) != len) throw new EOFException(); |
| 171 | return new String(buf, "UTF-8"); |
| 172 | } |
| 173 | |
| 174 | private Long decodeInt(InputStream in) throws IOException { |
| 175 | StringBuilder sb = new StringBuilder(); |
| 176 | while (true) { |
| 177 | int c = in.read(); |
| 178 | if (c == 'e') break; |
| 179 | sb.append((char) c); |
| 180 | } |
| 181 | return Long.parseLong(sb.toString()); |
| 182 | } |
| 183 | |
| 184 | // --- 编码 --- |
| 185 | private void encode(Object obj, OutputStream out) throws IOException { |
| 186 | if (obj instanceof String) encodeString((String) obj, out); |
| 187 | else if (obj instanceof Integer || obj instanceof Long) { |
| 188 | out.write('i'); |
| 189 | out.write(obj.toString().getBytes()); |
| 190 | out.write('e'); |
| 191 | } else if (obj instanceof List) { |
| 192 | out.write('l'); |
| 193 | for (Object item : (List<?>) obj) encode(item, out); |
| 194 | out.write('e'); |
| 195 | } else if (obj instanceof Map) { |
| 196 | out.write('d'); |
| 197 | Map<String, Object> map = (Map<String, Object>) obj; |
| 198 | List<String> keys = new ArrayList<>(map.keySet()); |
| 199 | Collections.sort(keys); |
| 200 | for (String key : keys) { |
| 201 | encodeString(key, out); |
| 202 | encode(map.get(key), out); |
| 203 | } |
| 204 | out.write('e'); |
| 205 | } else if (obj == null) { |
| 206 | // 跳过 |
| 207 | } else { |
| 208 | throw new IOException("Unsupported type: " + obj.getClass()); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | private void encodeString(String str, OutputStream out) throws IOException { |
| 213 | byte[] bytes = str.getBytes("UTF-8"); |
| 214 | out.write(Integer.toString(bytes.length).getBytes()); |
| 215 | out.write(':'); |
| 216 | out.write(bytes); |
| 217 | } |
| 218 | } |