增加流量监测和假种检测功能
Change-Id: I808ce14b6f08565f797f4681a6f72db9c730d011
diff --git a/src/main/java/cheat/Cheat.java b/src/main/java/cheat/Cheat.java
index cb35c05..1141c5b 100644
--- a/src/main/java/cheat/Cheat.java
+++ b/src/main/java/cheat/Cheat.java
@@ -4,6 +4,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.HashMap;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
@@ -20,6 +21,7 @@
import com.querydsl.jpa.impl.JPAQueryFactory;
+import entity.Seed;
import entity.Appeal;
import entity.QAppeal;
import entity.QSeed;
@@ -27,6 +29,8 @@
import entity.config;
import entity.QUser;
import entity.TTorent;
+import entity.QTransRecord;
+import entity.TransRecord;
public class Cheat implements CheatInterfnterface {
@@ -180,50 +184,230 @@
@Override
public void DetectTrans() {
- // JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
- // QSeed qSeed = QSeed.seed;
- // List<Seed> seeds = queryFactory
- // .selectFrom(qSeed)
- // .fetch();
- // for (Seed seed : seeds) {
- // QTransRecord qTransRecord = QTransRecord.transRecord;
- // List<TransRecord> tasks = queryFactory
- // .selectFrom(qTransRecord)
- // .where(qTransRecord.seedid.eq(seed.seedid))
- // .fetch();
+ EntityManager entityManager = emf.createEntityManager();
+ try {
+ JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
+ QSeed qSeed = QSeed.seed;
+ QTransRecord qTransRecord = QTransRecord.transRecord;
+
+ // 获取所有种子
+ List<Seed> seeds = queryFactory
+ .selectFrom(qSeed)
+ .fetch();
- // int n = tasks.size();
- // if (n == 0) continue;
+ // 存储每个种子的delta值
+ List<Double> allDeltas = new ArrayList<>();
+ Map<String, Double> seedDeltaMap = new HashMap<>();
+
+ // 计算每个种子的delta值(只考虑account_status不为1的用户)
+ for (Seed seed : seeds) {
+ List<TransRecord> tasks = queryFactory
+ .selectFrom(qTransRecord)
+ .leftJoin(qTransRecord.uploader).fetchJoin()
+ .leftJoin(qTransRecord.downloader).fetchJoin()
+ .where(qTransRecord.seedid.eq(seed.seedid))
+ .fetch();
- // double[] xArr = new double[n];
- // for (int i = 0; i < n; i++) {
- // TransRecord t = tasks.get(i);
- // xArr[i] = Math.max(0, t.upload - t.download);
- // }
+ // 过滤掉account_status为1的用户的记录
+ List<TransRecord> validTasks = new ArrayList<>();
+ for (TransRecord task : tasks) {
+ boolean shouldInclude = true;
+
+ // 检查上传者
+ if (task.uploaduserid != null) {
+ User uploader = entityManager.find(User.class, task.uploaduserid);
+ if (uploader != null && uploader.accountstate) {
+ shouldInclude = false;
+ }
+ }
+
+ // 检查下载者
+ if (task.downloaduserid != null) {
+ User downloader = entityManager.find(User.class, task.downloaduserid);
+ if (downloader != null && downloader.accountstate) {
+ shouldInclude = false;
+ }
+ }
+
+ if (shouldInclude) {
+ validTasks.add(task);
+ }
+ }
- // double sum = 0;
- // for (double x : xArr) sum += x;
- // double mu = sum / n;
+ if (validTasks.isEmpty()) continue;
- // double sqSum = 0;
- // for (double x : xArr) sqSum += (x - mu) * (x - mu);
- // double sigma = Math.sqrt(sqSum / n);
+ // 计算该种子的总delta
+ double totalDelta = 0;
+ for (TransRecord task : validTasks) {
+ long upload = task.upload != null ? task.upload : 0L;
+ long download = task.download != null ? task.download : 0L;
+ totalDelta += (upload - download);
+ }
- // for (int i = 0; i < n; i++) {
- // if (Math.abs(xArr[i] - mu) > 3 * sigma) {
- // User user = entityManager.find(User.class, tasks.get(i).downloaduserid);
- // if (user != null) {
- // user.detectedCount++;
- // user.lastDetectedTime = new java.util.Date();
- // entityManager.merge(user);
- // }
- // }
- // }
- // }
+ allDeltas.add(totalDelta);
+ seedDeltaMap.put(seed.seedid, totalDelta);
+ }
+
+ if (allDeltas.size() < 2) return; // 需要至少2个种子才能计算标准差
+
+ // 计算平均值和标准差
+ double sum = 0;
+ for (double delta : allDeltas) {
+ sum += delta;
+ }
+ double mean = sum / allDeltas.size();
+
+ double variance = 0;
+ for (double delta : allDeltas) {
+ variance += Math.pow(delta - mean, 2);
+ }
+ double stdDev = Math.sqrt(variance / allDeltas.size());
+
+ // 检测异常种子并处理参与者
+ for (Seed seed : seeds) {
+ Double seedDelta = seedDeltaMap.get(seed.seedid);
+ if (seedDelta == null) continue;
+
+ // 检查是否偏离1个标准差
+ if (Math.abs(seedDelta - mean) > stdDev) {
+ // 获取该种子的所有传输记录参与者
+ List<TransRecord> suspiciousTasks = queryFactory
+ .selectFrom(qTransRecord)
+ .where(qTransRecord.seedid.eq(seed.seedid))
+ .fetch();
+
+ // 更新所有参与者的detectedCount
+ for (TransRecord task : suspiciousTasks) {
+ // 处理上传者
+ if (task.uploaduserid != null) {
+ User uploader = entityManager.find(User.class, task.uploaduserid);
+ if (uploader != null && !uploader.accountstate) {
+ uploader.detectedCount++;
+ uploader.lastDetectedTime = new java.util.Date();
+ entityManager.merge(uploader);
+ }
+ }
+
+ // 处理下载者
+ if (task.downloaduserid != null) {
+ User downloader = entityManager.find(User.class, task.downloaduserid);
+ if (downloader != null && !downloader.accountstate) {
+ downloader.detectedCount++;
+ downloader.lastDetectedTime = new java.util.Date();
+ entityManager.merge(downloader);
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ if (entityManager != null) {
+ entityManager.close();
+ }
+ }
}
@Override
+ @Transactional
public void DetectFakeSeed(){
+ EntityManager entityManager = emf.createEntityManager();
+ try {
+ String torrentDir = entity.config.TORRENT_STORAGE_DIR;
+ String scriptPath = "./src/main/java/cheat/fakeSeed.bash";
+
+ ProcessBuilder processBuilder = new ProcessBuilder("bash", scriptPath, torrentDir);
+ processBuilder.inheritIO();
+
+ Process process = processBuilder.start();
+ int exitCode = process.waitFor();
+
+ if (exitCode == 0) {
+ System.out.println("FakeSeed detection completed successfully");
+
+ // 处理检测结果
+ String failedDir = torrentDir + "/failed_torrents";
+ java.io.File failedDirFile = new java.io.File(failedDir);
+
+ // 获取失败种子文件列表
+ java.util.Set<String> failedTorrentNames = new java.util.HashSet<>();
+ if (failedDirFile.exists() && failedDirFile.isDirectory()) {
+ java.io.File[] failedFiles = failedDirFile.listFiles((dir, name) -> name.endsWith(".torrent"));
+ if (failedFiles != null) {
+ for (java.io.File file : failedFiles) {
+ failedTorrentNames.add(file.getName());
+ }
+ }
+ }
+
+ JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
+ QSeed qSeed = QSeed.seed;
+
+ // 收集假种子的拥有者
+ java.util.Set<String> fakeOwners = new java.util.HashSet<>();
+
+ // 更新数据库中的fake_hits字段
+ List<Seed> allSeeds = queryFactory
+ .selectFrom(qSeed)
+ .where(qSeed.url.isNotNull())
+ .fetch();
+
+ for (Seed seed : allSeeds) {
+ java.io.File seedFile = new java.io.File(seed.url);
+ String fileName = seedFile.getName();
+
+ if (failedTorrentNames.contains(fileName)) {
+ // 失败种子,fake_hits + 1
+ seed.faketime++;
+ seed.lastfakecheck = new java.util.Date();
+ fakeOwners.add(seed.seeduserid);
+ System.out.println("Found fake seed: " + seed.seedid + " (fake_hits: " + seed.faketime + ")");
+ } else {
+ // 正常种子,fake_hits 清零
+ if (seed.faketime > 0) {
+ seed.faketime = 0;
+ seed.lastfakecheck = new java.util.Date();
+ System.out.println("Reset fake_hits for seed: " + seed.seedid);
+ }
+ }
+ entityManager.merge(seed);
+ }
+
+ // 更新所有用户的 fakeDetectedCount 和 fakeLastDetectedTime
+ entity.QUser qUser = entity.QUser.user;
+ List<User> allUsers = queryFactory
+ .selectFrom(qUser)
+ .fetch();
+
+ java.util.Date now = new java.util.Date();
+ for (User user : allUsers) {
+ if (fakeOwners.contains(user.userid)) {
+ // 假种子拥有者,fakeDetectedCount + 1
+ user.fakeDetectedCount++;
+ System.out.println("Increased fake detection count for user: " + user.userid + " (count: " + user.fakeDetectedCount + ")");
+ } else {
+ // 其他用户,fakeDetectedCount 清零
+ if (user.fakeDetectedCount > 0) {
+ user.fakeDetectedCount = 0;
+ System.out.println("Reset fake detection count for user: " + user.userid);
+ }
+ }
+ // 更新所有用户的检测时间
+ user.fakeLastDetectedTime = now;
+ entityManager.merge(user);
+ }
+
+ System.out.println("Fake seed detection and database update completed");
+ } else {
+ System.err.println("FakeSeed detection failed with exit code: " + exitCode);
+ }
+ } catch (Exception e) {
+ System.err.println("Error running FakeSeed detection script: " + e.getMessage());
+ e.printStackTrace();
+ } finally {
+ if (entityManager != null) {
+ entityManager.close();
+ }
+ }
}
@Override
diff --git a/src/main/java/cheat/fakeSeed.bash b/src/main/java/cheat/fakeSeed.bash
new file mode 100644
index 0000000..a2aae52
--- /dev/null
+++ b/src/main/java/cheat/fakeSeed.bash
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+#
+# batch_test_torrents.sh — 批量检测 .torrent 文件是否能正确启动下载
+#
+# 依赖:curl, jq
+#
+# 用法:./batch_test_torrents.sh /path/to/torrent_dir
+
+set -euo pipefail
+
+QB_URL="http://127.0.0.1:8080"
+QB_USER="admin"
+QB_PASS="9H6k8VpcM"
+COOKIE_JAR="$(mktemp)"
+TORRENT_DIR="${1:-.}"
+TIMEOUT_SECS=60 # 最长等多久才判超时
+POLL_INTERVAL=2 # 每次轮询间隔
+FAILED_DIR="${TORRENT_DIR}/failed_torrents" # 失败种子存放目录
+
+
+# 登录
+_login() {
+ curl -s -c "$COOKIE_JAR" \
+ -d "username=$QB_USER&password=$QB_PASS" \
+ "$QB_URL/api/v2/auth/login" \
+ | grep -q "Ok." || {
+ echo "❌ 登录失败" >&2
+ exit 1
+ }
+}
+
+# 登出
+_logout() {
+ curl -s -b "$COOKIE_JAR" "$QB_URL/api/v2/auth/logout" >/dev/null
+ rm -f "$COOKIE_JAR"
+}
+
+# 添加 torrent,返回 infoHash
+# $1 = .torrent 文件路径
+_add_torrent() {
+ local file="$1"
+ # 丢弃 “Ok.”,只留下后续 info-hash
+ curl -s -b "$COOKIE_JAR" -X POST \
+ -F "torrents=@${file}" \
+ "$QB_URL/api/v2/torrents/add" >/dev/null
+
+ # 等待 qBittorrent 收到任务
+ sleep 3
+
+ # 取最新添加的那个 torrent(按 added_on 降序,limit=1)
+ local info
+ info=$(curl -s -b "$COOKIE_JAR" \
+ "$QB_URL/api/v2/torrents/info?limit=1&sort=added_on&reverse=true")
+
+ # 检查是否获取到有效的 JSON 响应
+ if ! echo "$info" | jq empty 2>/dev/null; then
+ echo "ERROR: Invalid JSON response from qBittorrent API" >&2
+ echo "Response: $info" >&2
+ return 1
+ fi
+
+ # 检查是否有 torrent 记录
+ local count
+ count=$(echo "$info" | jq 'length')
+ if [[ "$count" == "0" ]]; then
+ echo "ERROR: No torrents found after adding" >&2
+ return 1
+ fi
+
+ # 只输出 hash
+ echo "$info" | jq -r '.[0].hash'
+}
+
+# 删除 torrent,同时删除已下载的文件
+# $1 = infoHash
+_delete_torrent() {
+ local hash="$1"
+ curl -s -b "$COOKIE_JAR" \
+ -G --data-urlencode "hashes=${hash}" \
+ "$QB_URL/api/v2/torrents/delete?deleteFiles=true" >/dev/null
+}
+
+# 等待并检测状态
+# $1 = infoHash
+_wait_for_progress() {
+ local hash="$1"
+ local waited=0
+
+ while (( waited < TIMEOUT_SECS )); do
+ # 获取特定种子信息
+ local info
+ info=$(curl -s -b "$COOKIE_JAR" \
+ -G --data-urlencode "hashes=${hash}" \
+ "$QB_URL/api/v2/torrents/info")
+
+ # 检查 JSON 响应
+ if ! echo "$info" | jq empty 2>/dev/null; then
+ echo "⚠️ ${hash}: Invalid API response, retrying..."
+ sleep $POLL_INTERVAL
+ waited=$(( waited + POLL_INTERVAL ))
+ continue
+ fi
+
+ # 检查是否返回了数据
+ local count
+ count=$(echo "$info" | jq 'length')
+ if [[ "$count" == "0" ]]; then
+ echo "⚠️ ${hash}: Torrent not found, retrying..."
+ sleep $POLL_INTERVAL
+ waited=$(( waited + POLL_INTERVAL ))
+ continue
+ fi
+
+ local state progress
+ state=$(echo "$info" | jq -r '.[0].state // "unknown"')
+ progress=$(echo "$info" | jq -r '.[0].progress // 0')
+
+ # 成功开始下载(progress > 0)
+ if awk "BEGIN {exit !($progress > 0)}"; then
+ local progress_percent
+ progress_percent=$(awk "BEGIN {printf \"%.2f\", $progress * 100}")
+ echo "✅ ${hash}: started downloading (progress=${progress_percent}%)"
+ return 0
+ fi
+
+ # 出错状态
+ if [[ "$state" == "error" ]]; then
+ echo "❌ ${hash}: entered error state"
+ return 1
+ fi
+
+ sleep $POLL_INTERVAL
+ waited=$(( waited + POLL_INTERVAL ))
+ done
+
+ echo "⚠️ ${hash}: no progress after ${TIMEOUT_SECS}s timeout"
+ return 2
+}
+
+main() {
+ if [[ ! -d "$TORRENT_DIR" ]]; then
+ echo "Usage: $0 /path/to/torrent_dir" >&2
+ exit 1
+ fi
+
+ # 创建失败种子目录(如果不存在)
+ mkdir -p "$FAILED_DIR"
+
+ # 清空失败种子目录
+ if [[ -d "$FAILED_DIR" ]]; then
+ rm -f "$FAILED_DIR"/*.torrent 2>/dev/null || true
+ echo "已清空失败种子目录:$FAILED_DIR"
+ fi
+
+ _login
+ echo "开始批量测试目录:$TORRENT_DIR"
+ echo "失败的种子将被复制到:$FAILED_DIR"
+
+ for file in "$TORRENT_DIR"/*.torrent; do
+ [[ -e "$file" ]] || { echo "目录中没有 .torrent 文件"; break; }
+
+ echo "---- 测试 $file ----"
+ hash=$(_add_torrent "$file")
+ echo "添加成功,infoHash=$hash"
+
+ if _wait_for_progress "$hash"; then
+ echo ">>> $file 下载检测通过"
+ else
+ echo ">>> $file 下载检测失败"
+ cp "$file" "$FAILED_DIR/"
+ echo "已将失败种子复制到:$FAILED_DIR/$(basename "$file")"
+ fi
+
+ _delete_torrent "$hash"
+ echo
+ done
+
+ _logout
+ echo "全部完成。失败的种子文件已保存在:$FAILED_DIR"
+}
+
+main "$@"
diff --git a/src/main/java/cheat/faketest.py b/src/main/java/cheat/faketest.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/main/java/cheat/faketest.py
diff --git a/src/main/java/entity/config.java b/src/main/java/entity/config.java
index 0230066..c77f73d 100644
--- a/src/main/java/entity/config.java
+++ b/src/main/java/entity/config.java
@@ -85,4 +85,7 @@
begVote = 3;
cheatTime = 5;
}
+ // public static final String trackerHost="0.0.0.0";
+ // public static final int trackerPort=6969;
+ // public static final int capturePort=6670;
}
diff --git a/src/main/java/tracker/DataCaptureProxy.java b/src/main/java/tracker/DataCaptureProxy.java
index cb66feb..1749421 100644
--- a/src/main/java/tracker/DataCaptureProxy.java
+++ b/src/main/java/tracker/DataCaptureProxy.java
@@ -66,14 +66,22 @@
if (uploaded != null && !uploaded.isEmpty()) {
int uploadValue = Integer.parseInt(uploaded);
if (uploadValue > 0) {
- tracker.AddUpLoad(passkey, uploadValue, infoHash);
+ try {
+ tracker.AddUpLoad(passkey, uploadValue, infoHash);
+ } catch (javax.persistence.NoResultException e) {
+ System.out.println("Skipping upload update: info_hash not found in database - " + infoHash);
+ }
}
}
if (downloaded != null && !downloaded.isEmpty()) {
int downloadValue = Integer.parseInt(downloaded);
if (downloadValue > 0) {
- tracker.AddDownload(passkey, downloadValue, infoHash);
+ try {
+ tracker.AddDownload(passkey, downloadValue, infoHash);
+ } catch (javax.persistence.NoResultException e) {
+ System.out.println("Skipping download update: info_hash not found in database - " + infoHash);
+ }
}
}
} catch (NumberFormatException e) {
diff --git a/src/main/java/tracker/Tracker.java b/src/main/java/tracker/Tracker.java
index 1ac4d94..1b38806 100644
--- a/src/main/java/tracker/Tracker.java
+++ b/src/main/java/tracker/Tracker.java
@@ -16,7 +16,8 @@
import com.querydsl.jpa.impl.JPAUpdateClause;
import entity.*;
import entity.config;
-
+import java.util.Scanner;
+import java.io.IOException;
public class Tracker implements TrackerInterface {
private final EntityManagerFactory emf;
// 默认构造:产线数据库
@@ -55,7 +56,7 @@
long delta = newTotal - sumSoFar;
if (delta < 0L) {
tx.rollback();
- return true; // error: newTotal less than already recorded
+ return false; // error: newTotal less than already recorded
}
if (delta == 0L) {
tx.rollback();
@@ -69,7 +70,7 @@
rd.upload = delta;
rd.maxupload = newTotal;
em.persist(rd);
-
+ em.flush();
// 4) 重新计算用户的总上传,确保与 TransRecord 完全一致
Long totalUpload = em.createQuery(
"SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid",
@@ -77,10 +78,29 @@
)
.setParameter("uid", userid)
.getSingleResult();
+
+ Long PTuploadbefor = em.createQuery(
+ "SELECT t.upload FROM UserPT t WHERE t.userid = :uid",
+ Long.class
+ ).setParameter("uid", userid)
+ .getSingleResult();
+
+
UserPT user = em.find(UserPT.class, userid);
user.upload = totalUpload;
em.merge(user);
+ em.flush();
tx.commit();
+
+ Long PTuploadafter = em.createQuery(
+ "SELECT t.upload FROM UserPT t WHERE t.userid = :uid",
+ Long.class
+ ).setParameter("uid", userid)
+ .getSingleResult();
+
+ System.out.println("按回车继续...");
+ System.out.printf("thisadd:%d userptsofar:%d userptafter:%d totaluploadnow:%d delta:%d%n",upload, PTuploadbefor,PTuploadafter, totalUpload,delta);
+ System.out.println("按回车继续...");
return false; // success
} catch (RuntimeException ex) {
if (tx.isActive()) tx.rollback();
@@ -137,7 +157,7 @@
List<SeedHash> shl = qsh.getResultList();
if (shl.isEmpty()) {
System.out.println("seed没有被记录");
- return true;
+ return false;
}
String seedid = shl.get(0).seedId;
@@ -202,7 +222,7 @@
long current = qcurr.getSingleResult();
if (downloadLong > current) {
em.close();
- return true;
+ return false;
}
// 2. 执行减法更新
EntityTransaction tx = em.getTransaction();
@@ -271,14 +291,16 @@
Path target = storageDir.resolve(seedid + "_" + filename);
Files.copy(TTorent.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
- // attempt to parse infoHash, but don’t fail if parsing fails
+ // attempt to parse infoHash, but don't fail if parsing fails
String infoHash = null;
try {
byte[] torrentData = Files.readAllBytes(target);
Bencode bencode = new Bencode();
@SuppressWarnings("unchecked")
- Map<String,Object> meta = (Map<String,Object>) bencode.decode(torrentData, Type.DICTIONARY);
- byte[] infoBytes = bencode.encode((Map<String,Object>) meta.get("info"));
+ Map<String,Object> meta = bencode.decode(torrentData, Type.DICTIONARY);
+ @SuppressWarnings("unchecked")
+ Map<String,Object> info = (Map<String,Object>) meta.get("info");
+ byte[] infoBytes = bencode.encode(info);
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] digest = sha1.digest(infoBytes);
StringBuilder sb = new StringBuilder();