diff --git a/pom.xml b/pom.xml
index 3d4412d305..5e637c4d8a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.ghostchu.peerbanhelper
peerbanhelper
- 4.2.4
+ 4.3.0
takari-jar
PeerBanHelper
@@ -14,7 +14,7 @@
21
UTF-8
- com.ghostchu.peerbanhelper.Main
+ com.ghostchu.peerbanhelper.MainJumpLoader
yyyyMMdd-HHmmss
3.4.1
5.8.27
@@ -485,5 +485,11 @@
libby-standalone
2.0.2-SNAPSHOT
+
+
+ org.json
+ json
+ 20240303
+
diff --git a/src/main/java/com/ghostchu/peerbanhelper/MainJumpLoader.java b/src/main/java/com/ghostchu/peerbanhelper/MainJumpLoader.java
new file mode 100644
index 0000000000..4fab138c5c
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/MainJumpLoader.java
@@ -0,0 +1,47 @@
+package com.ghostchu.peerbanhelper;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class MainJumpLoader {
+ public static void main(String[] args) {
+ // Do something before real Main class
+ if (System.getProperty("os.name").toLowerCase().contains("windows")) {
+ setupCharsets();
+ }
+ Main.main(args);
+ }
+
+ private static void setupCharsets() {
+ try {
+ invokeCommand("cmd.exe /c chcp 65001", Collections.emptyMap());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static int invokeCommand(String command, Map env) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ StringTokenizer st = new StringTokenizer(command);
+ String[] cmdarray = new String[st.countTokens()];
+ for (int i = 0; st.hasMoreTokens(); i++) {
+ cmdarray[i] = st.nextToken();
+ }
+ ProcessBuilder builder = new ProcessBuilder(cmdarray)
+ .inheritIO();
+ Map liveEnv = builder.environment();
+ liveEnv.putAll(env);
+ Process p = builder.start();
+ Process process = p.onExit().get(10, TimeUnit.SECONDS);
+ if (process.isAlive()) {
+ process.destroy();
+ return -9999;
+ }
+ return process.exitValue();
+ }
+
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java b/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java
index d34f03b7f8..2dad0134b2 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java
@@ -7,6 +7,7 @@
import com.ghostchu.peerbanhelper.downloader.Downloader;
import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus;
import com.ghostchu.peerbanhelper.downloader.impl.biglybt.BiglyBT;
+import com.ghostchu.peerbanhelper.downloader.impl.deluge.Deluge;
import com.ghostchu.peerbanhelper.downloader.impl.qbittorrent.QBittorrent;
import com.ghostchu.peerbanhelper.downloader.impl.transmission.Transmission;
import com.ghostchu.peerbanhelper.event.LivePeersUpdatedEvent;
@@ -168,6 +169,7 @@ public Downloader createDownloader(String client, ConfigurationSection downloade
case "transmission" ->
downloader = Transmission.loadFromConfig(client, pbhServerAddress, downloaderSection);
case "biglybt" -> downloader = BiglyBT.loadFromConfig(client, downloaderSection);
+ case "deluge" -> downloader = Deluge.loadFromConfig(client, downloaderSection);
}
return downloader;
@@ -180,6 +182,7 @@ public Downloader createDownloader(String client, JsonObject downloaderSection)
case "transmission" ->
downloader = Transmission.loadFromConfig(client, pbhServerAddress, downloaderSection);
case "biglybt" -> downloader = BiglyBT.loadFromConfig(client, downloaderSection);
+ case "deluge" -> downloader = Deluge.loadFromConfig(client, downloaderSection);
}
return downloader;
@@ -433,7 +436,7 @@ public void banWave() {
});
});
- needRelaunched.put(downloader, relaunch);
+ needRelaunched.put(downloader, relaunch);
} catch (Exception e) {
log.error("Unable to complete peer ban task, report to PBH developer!!!");
}
@@ -573,8 +576,12 @@ public Map>> collectPeers() {
Map>> peers = new HashMap<>();
try (var service = Executors.newVirtualThreadPerTaskExecutor()) {
downloaders.forEach(downloader -> service.submit(() -> {
- Map> p = collectPeers(downloader);
- peers.put(downloader, p);
+ try {
+ Map> p = collectPeers(downloader);
+ peers.put(downloader, p);
+ } catch (Exception e) {
+ log.warn(Lang.DOWNLOADER_UNHANDLED_EXCEPTION, e);
+ }
}));
}
return peers;
diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java
index 03894a2def..7f746b1eb3 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java
@@ -5,6 +5,7 @@
import com.ghostchu.peerbanhelper.util.rule.MatchResult;
import com.ghostchu.peerbanhelper.util.rule.Rule;
import com.ghostchu.peerbanhelper.util.rule.RuleParser;
+import com.ghostchu.peerbanhelper.util.rule.matcher.IPMatcher;
import inet.ipaddr.IPAddress;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
@@ -69,13 +70,7 @@ public String matcherIdentifier() {
public Map> parseIPRule(Map> raw) {
Map> rules = new HashMap<>();
- raw.forEach((k, v) -> {
- List addresses = new ArrayList<>();
- for (String s : v) {
- addresses.add(new BtnRuleIpMatcher(IPAddressUtil.getIPAddress(s)));
- }
- rules.put(k, addresses);
- });
+ raw.forEach((k, v) -> rules.put(k, List.of(new BtnRuleIpMatcher(k, k, v.stream().map(IPAddressUtil::getIPAddress).toList()))));
return rules;
}
@@ -85,37 +80,14 @@ public Map> parseRule(Map> raw) {
return rules;
}
- public static class BtnRuleIpMatcher implements Rule {
- private IPAddress ipAddress;
-
- public BtnRuleIpMatcher(IPAddress ipAddress) {
- this.ipAddress = ipAddress;
- if (this.ipAddress.isIPv4Convertible()) {
- this.ipAddress = this.ipAddress.toIPv4();
- }
- }
+ public static class BtnRuleIpMatcher extends IPMatcher {
- @Override
- public @NotNull MatchResult match(@NotNull String content) {
- Main.getServer().getHitRateMetric().addQuery(this);
- IPAddress contentAddr = IPAddressUtil.getIPAddress(content);
- if (contentAddr.isIPv4Convertible()) {
- contentAddr = contentAddr.toIPv4();
- }
- MatchResult result = (ipAddress.contains(contentAddr) || ipAddress.equals(contentAddr)) ? MatchResult.TRUE : MatchResult.DEFAULT;
- if (result != MatchResult.DEFAULT) {
- Main.getServer().getHitRateMetric().addHit(this);
- }
- return result;
- }
-
- @Override
- public Map metadata() {
- return Map.of("ip", this.ipAddress.toString());
+ public BtnRuleIpMatcher(String ruleId, String ruleName, List ruleData) {
+ super(ruleId, ruleName, ruleData);
}
@Override
- public String matcherName() {
+ public @NotNull String matcherName() {
return "BTN-IP";
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java b/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java
index e41fdba12a..584ddd1929 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java
@@ -26,6 +26,12 @@ private void validate() {
}
}
+ @UpdateScript(version = 9)
+ public void firewallIntegration() {
+ conf.set("firewall-integration", null);
+ conf.set("firewall-integration.windows-adv-firewall", true);
+ }
+
@UpdateScript(version = 8)
public void webToken() {
conf.set("server.token", UUID.randomUUID().toString());
diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java
new file mode 100644
index 0000000000..084acc3e47
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java
@@ -0,0 +1,359 @@
+package com.ghostchu.peerbanhelper.downloader.impl.deluge;
+
+import com.ghostchu.peerbanhelper.downloader.Downloader;
+import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth;
+import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus;
+import com.ghostchu.peerbanhelper.downloader.WebViewScriptCallback;
+import com.ghostchu.peerbanhelper.peer.Peer;
+import com.ghostchu.peerbanhelper.text.Lang;
+import com.ghostchu.peerbanhelper.torrent.Torrent;
+import com.ghostchu.peerbanhelper.util.JsonUtil;
+import com.ghostchu.peerbanhelper.wrapper.BanMetadata;
+import com.ghostchu.peerbanhelper.wrapper.PeerAddress;
+import com.ghostchu.peerbanhelper.wrapper.TorrentWrapper;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.SneakyThrows;
+import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection;
+import org.bspfsystems.yamlconfiguration.file.YamlConfiguration;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import raccoonfink.deluge.DelugeException;
+import raccoonfink.deluge.DelugeServer;
+import raccoonfink.deluge.responses.DelugeListMethodsResponse;
+import raccoonfink.deluge.responses.PBHActiveTorrentsResponse;
+
+import java.net.http.HttpClient;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class Deluge implements Downloader {
+ private static final Logger log = org.slf4j.LoggerFactory.getLogger(Deluge.class);
+ private static final List MUST_HAVE_METHODS = ImmutableList.of(
+ "peerbanhelperadapter.replace_blocklist",
+ "peerbanhelperadapter.unban_ips",
+ "peerbanhelperadapter.get_active_torrents_info",
+ "peerbanhelperadapter.ban_ips"
+ );
+ private final String name;
+ private final DelugeServer client;
+ private final Config config;
+ private DownloaderLastStatus lastStatus = DownloaderLastStatus.UNKNOWN;
+ private String statusMessage;
+
+ public Deluge(String name, Config config) {
+ this.name = name;
+ this.config = config;
+ this.client = new DelugeServer(config.getEndpoint() + config.getRpcUrl(), config.getPassword(), config.isVerifySsl(), HttpClient.Version.valueOf(config.getHttpVersion()), null, null);
+ }
+
+ public static Deluge loadFromConfig(String name, ConfigurationSection section) {
+ Config config = Config.readFromYaml(section);
+ return new Deluge(name, config);
+ }
+
+ public static Deluge loadFromConfig(String name, JsonObject section) {
+ Config config = JsonUtil.getGson().fromJson(section.toString(), Config.class);
+ return new Deluge(name, config);
+ }
+
+ private static String toStringHex(String s) {
+ byte[] baKeyword = new byte[s.length() / 2];
+ for (int i = 0; i < baKeyword.length; i++) {
+ baKeyword[i] = (byte) (0xff & Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16));
+ }
+ return new String(baKeyword, StandardCharsets.ISO_8859_1);
+ }
+
+ @Override
+ public JsonObject saveDownloaderJson() {
+ return JsonUtil.getGson().toJsonTree(config).getAsJsonObject();
+ }
+
+ @Override
+ public YamlConfiguration saveDownloader() {
+ return config.saveToYaml();
+ }
+
+ @Override
+ public String getEndpoint() {
+ return config.getEndpoint();
+ }
+
+ @Override
+ public String getWebUIEndpoint() {
+ return config.getEndpoint();
+ }
+
+ @Override
+ public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() {
+ return null;
+ }
+
+ @Override
+ public @Nullable WebViewScriptCallback getWebViewJavaScript() {
+ return null;
+ }
+
+ @Override
+ public boolean isSupportWebview() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getType() {
+ return "Deluge";
+ }
+
+ @Override
+ public boolean login() {
+ try {
+ if (!this.client.login().isLoggedIn()) {
+ return false;
+ }
+ DelugeListMethodsResponse listMethodsResponse = this.client.listMethods();
+ if (!new HashSet<>(listMethodsResponse.getDelugeSupportedMethods()).containsAll(MUST_HAVE_METHODS)) {
+ log.warn(Lang.DOWNLOADER_DELUGE_PLUGIN_NOT_INSTALLED, getName());
+ return false;
+ }
+ } catch (DelugeException e) {
+ throw new RuntimeException(e);
+ }
+ return true;
+ }
+
+ @Override
+ public List getTorrents() {
+ List torrents = new ArrayList<>();
+ try {
+ for (PBHActiveTorrentsResponse.ActiveTorrentsResponseDTO activeTorrent : this.client.getActiveTorrents().getActiveTorrents()) {
+ List peers = new ArrayList<>();
+ for (PBHActiveTorrentsResponse.ActiveTorrentsResponseDTO.PeersDTO peer : activeTorrent.getPeers()) {
+ DelugePeer delugePeer = new DelugePeer(
+ new PeerAddress(peer.getIp(), peer.getPort()),
+ toStringHex(peer.getPeerId()),
+ peer.getClientName(),
+ peer.getTotalDownload(),
+ peer.getPayloadDownSpeed(),
+ peer.getTotalUpload(),
+ peer.getPayloadUpSpeed(),
+ peer.getProgress() / 100.0d,
+ parseFlag(peer.getFlags(), peer.getSource())
+ );
+ peers.add(delugePeer);
+ }
+ Torrent torrent = new DelugeTorrent(
+ activeTorrent.getId(),
+ activeTorrent.getName(),
+ activeTorrent.getInfoHash(),
+ activeTorrent.getProgress() / 100.0d,
+ activeTorrent.getSize(),
+ activeTorrent.getUploadPayloadRate(),
+ activeTorrent.getDownloadPayloadRate(),
+ peers
+ );
+ torrents.add(torrent);
+ }
+ } catch (DelugeException e) {
+ log.warn(Lang.DOWNLOADER_DELUGE_API_ERROR, e);
+ }
+ return torrents;
+ }
+
+ @Override
+ public List getPeers(Torrent torrent) {
+ if (!(torrent instanceof DelugeTorrent delugeTorrent)) {
+ throw new IllegalStateException("The torrent object not a instance of DelugeTorrent");
+ }
+ return delugeTorrent.getPeers();
+ }
+
+ @SneakyThrows
+ @Override
+ public void setBanList(Collection fullList, @Nullable Collection added, @Nullable Collection removed) {
+ if (removed != null && removed.isEmpty() && added != null && config.isIncrementBan()) {
+ setBanListIncrement(added);
+ } else {
+ setBanListFull(fullList);
+ }
+ }
+
+ private void setBanListFull(Collection fullList) {
+ try {
+ this.client.replaceBannedPeers(fullList.stream().map(PeerAddress::getIp).toList());
+ } catch (DelugeException e) {
+ log.warn(Lang.DOWNLOADER_DELUGE_API_ERROR, e);
+ }
+ }
+
+ private void setBanListIncrement(Collection added) {
+ try {
+ this.client.banPeers(added.stream().map(bm -> bm.getPeer().getAddress().getIp()).toList());
+ } catch (DelugeException e) {
+ log.warn(Lang.DOWNLOADER_DELUGE_API_ERROR, e);
+ }
+ }
+
+ @Override
+ public void relaunchTorrentIfNeeded(Collection torrents) {
+
+ }
+
+ @Override
+ public void relaunchTorrentIfNeededByTorrentWrapper(Collection torrents) {
+
+ }
+
+ @Override
+ public DownloaderLastStatus getLastStatus() {
+ return lastStatus;
+ }
+
+ @Override
+ public void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage) {
+ this.lastStatus = lastStatus;
+ this.statusMessage = statusMessage;
+ }
+
+ @Override
+ public String getLastStatusMessage() {
+ return statusMessage;
+ }
+
+ @Override
+ public void close() {
+
+ }
+ private String parseFlag(int peerFlag, int sourceFlag) {
+ boolean interesting = (peerFlag & (1 << 0)) != 0;
+ boolean choked = (peerFlag & (1 << 1)) != 0;
+ boolean remoteInterested = (peerFlag & (1 << 2)) != 0;
+ boolean remoteChoked = (peerFlag & (1 << 3)) != 0;
+ boolean supportsExtensions = (peerFlag & (1 << 4)) != 0;
+ boolean outgoingConnection = (peerFlag & (1 << 5)) != 0;
+ boolean localConnection = (peerFlag & (1 << 6)) != 0;
+ boolean handshake = (peerFlag & (1 << 7)) != 0;
+ boolean connecting = (peerFlag & (1 << 8)) != 0;
+ boolean onParole = (peerFlag & (1 << 9)) != 0;
+ boolean seed = (peerFlag & (1 << 10)) != 0;
+ boolean optimisticUnchoke = (peerFlag & (1 << 11)) != 0;
+ boolean snubbed = (peerFlag & (1 << 12)) != 0;
+ boolean uploadOnly = (peerFlag & (1 << 13)) != 0;
+ boolean endGameMode = (peerFlag & (1 << 14)) != 0;
+ boolean holePunched = (peerFlag & (1 << 15)) != 0;
+ boolean i2pSocket = (peerFlag & (1 << 16)) != 0;
+ boolean utpSocket = (peerFlag & (1 << 17)) != 0;
+ boolean sslSocket = (peerFlag & (1 << 18)) != 0;
+ boolean rc4Encrypted = (peerFlag & (1 << 19)) != 0;
+ boolean plainTextEncrypted = (peerFlag & (1 << 20)) != 0;
+
+ boolean tracker = (sourceFlag & (1 << 0)) != 0;
+ boolean dht = (sourceFlag & (1 << 1)) != 0;
+ boolean pex = (sourceFlag & (1 << 2)) != 0;
+ boolean lsd = (sourceFlag & (1 << 3)) != 0;
+ boolean resumeData = (sourceFlag & (1 << 4)) != 0;
+ boolean incoming = (sourceFlag & (1 << 5)) != 0;
+
+ StringJoiner joiner = new StringJoiner(" ");
+
+ if (interesting) {
+ if (remoteChoked) {
+ joiner.add("d");
+ } else {
+ joiner.add("D");
+ }
+ }
+ if (remoteInterested) {
+ if (choked) {
+ joiner.add("u");
+ } else {
+ joiner.add("U");
+ }
+ }
+ if (!remoteChoked && !interesting)
+ joiner.add("K");
+ if (!choked && !remoteInterested)
+ joiner.add("?");
+ if (optimisticUnchoke)
+ joiner.add("O");
+ if (snubbed)
+ joiner.add("S");
+ if (!localConnection)
+ joiner.add("I");
+ if (dht)
+ joiner.add("H");
+ if (pex)
+ joiner.add("X");
+ if (lsd)
+ joiner.add("L");
+ if (rc4Encrypted)
+ joiner.add("E");
+ if (plainTextEncrypted)
+ joiner.add("e");
+ if (utpSocket)
+ joiner.add("P");
+
+ return joiner.toString();
+ }
+
+
+ private boolean c2b(char c) {
+ return c == '1';
+ }
+
+ private String readBits(int i, int bitLength) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(Integer.toBinaryString(i));
+ while (builder.length() < bitLength) {
+ builder.append("0");
+ }
+ return builder.toString();
+ }
+
+ @NoArgsConstructor
+ @Data
+ public static class Config {
+
+ private String type;
+ private String endpoint;
+ private String password;
+ private String httpVersion;
+ private boolean verifySsl;
+ private String rpcUrl;
+ private boolean incrementBan;
+
+ public static Config readFromYaml(ConfigurationSection section) {
+ Config config = new Config();
+ config.setType("deluge");
+ config.setEndpoint(section.getString("endpoint"));
+ if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况
+ config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1));
+ }
+ config.setPassword(section.getString("password"));
+ config.setRpcUrl(section.getString("rpc-url", "/json"));
+ config.setHttpVersion(section.getString("http-version", "HTTP_1_1"));
+ config.setVerifySsl(section.getBoolean("verify-ssl", true));
+ config.setIncrementBan(section.getBoolean("increment-ban"));
+ return config;
+ }
+
+ public YamlConfiguration saveToYaml() {
+ YamlConfiguration section = new YamlConfiguration();
+ section.set("type", "deluge");
+ section.set("endpoint", endpoint);
+ section.set("password", password);
+ section.set("rpc-url", rpcUrl);
+ section.set("http-version", httpVersion);
+ section.set("increment-ban", incrementBan);
+ section.set("verify-ssl", verifySsl);
+ return section;
+ }
+ }
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java
new file mode 100644
index 0000000000..40c9c3ab69
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java
@@ -0,0 +1,20 @@
+package com.ghostchu.peerbanhelper.downloader.impl.deluge;
+
+import com.ghostchu.peerbanhelper.peer.Peer;
+import com.ghostchu.peerbanhelper.wrapper.PeerAddress;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class DelugePeer implements Peer {
+ private PeerAddress peerAddress;
+ private String peerId;
+ private String clientName;
+ private long downloaded;
+ private long downloadSpeed;
+ private long uploaded;
+ private long uploadSpeed;
+ private double progress;
+ private String flags;
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java
new file mode 100644
index 0000000000..bc2a34729d
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java
@@ -0,0 +1,21 @@
+package com.ghostchu.peerbanhelper.downloader.impl.deluge;
+
+import com.ghostchu.peerbanhelper.peer.Peer;
+import com.ghostchu.peerbanhelper.torrent.Torrent;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+public class DelugeTorrent implements Torrent {
+ private String id;
+ private String name;
+ private String hash;
+ private double progress;
+ private long size;
+ private long rtUploadSpeed;
+ private long rtDownloadSpeed;
+ private List peers;
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java
index e64b41d47b..43832c8d39 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java
@@ -235,7 +235,7 @@ public static Transmission.Config readFromYaml(ConfigurationSection section) {
}
config.setUsername(section.getString("username"));
config.setPassword(section.getString("password"));
- config.setRpcUrl(section.getString("rpc-url", "transmission/rpc"));
+ config.setRpcUrl(section.getString("rpc-url", "/transmission/rpc"));
config.setHttpVersion(section.getString("http-version", "HTTP_1_1"));
config.setVerifySsl(section.getBoolean("verify-ssl", true));
return config;
diff --git a/src/main/java/com/ghostchu/peerbanhelper/firewall/Firewall.java b/src/main/java/com/ghostchu/peerbanhelper/firewall/Firewall.java
new file mode 100644
index 0000000000..949984f8d8
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/firewall/Firewall.java
@@ -0,0 +1,19 @@
+package com.ghostchu.peerbanhelper.firewall;
+
+import inet.ipaddr.IPAddress;
+
+public interface Firewall {
+ String getName();
+
+ boolean isApplicable();
+
+ boolean ban(IPAddress address) throws Exception;
+
+ boolean unban(IPAddress address) throws Exception;
+
+ boolean reset() throws Exception;
+
+ boolean load() throws Exception;
+
+ boolean unload() throws Exception;
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/firewall/FirewallManager.java b/src/main/java/com/ghostchu/peerbanhelper/firewall/FirewallManager.java
new file mode 100644
index 0000000000..e92d4771b5
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/firewall/FirewallManager.java
@@ -0,0 +1,25 @@
+package com.ghostchu.peerbanhelper.firewall;
+
+import com.ghostchu.peerbanhelper.PeerBanHelperServer;
+import com.ghostchu.peerbanhelper.firewall.impl.LocalWindowsAdvFirewall;
+import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class FirewallManager {
+ private final PeerBanHelperServer server;
+ private Set enabledFirewalls = new HashSet<>();
+
+ public FirewallManager(PeerBanHelperServer server) {
+ this.server = server;
+ }
+
+ private void reloadConfig() {
+ ConfigurationSection section = server.getMainConfig().getConfigurationSection("firewall-integration");
+ if (section == null) throw new IllegalArgumentException("The firewall-integration section cannot be null");
+ if (section.getBoolean("windows-adv-firewall")) {
+ enabledFirewalls.add(new LocalWindowsAdvFirewall(server));
+ }
+ }
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java b/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java
new file mode 100644
index 0000000000..28672864d3
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java
@@ -0,0 +1,35 @@
+package com.ghostchu.peerbanhelper.firewall.impl;
+
+import com.ghostchu.peerbanhelper.text.Lang;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@Slf4j
+public class CommandBasedImpl {
+
+ public int invokeCommand(String command, Map env) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ StringTokenizer st = new StringTokenizer(command);
+ String[] cmdarray = new String[st.countTokens()];
+ for (int i = 0; st.hasMoreTokens(); i++) {
+ cmdarray[i] = st.nextToken();
+ }
+ ProcessBuilder builder = new ProcessBuilder(cmdarray)
+ .inheritIO();
+ Map liveEnv = builder.environment();
+ liveEnv.putAll(env);
+ Process p = builder.start();
+ Process process = p.onExit().get(10, TimeUnit.SECONDS);
+ if (process.isAlive()) {
+ process.destroy();
+ log.warn(Lang.COMMAND_EXECUTOR_FAILED_TIMEOUT, command);
+ return -9999;
+ }
+ return process.exitValue();
+ }
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/LocalWindowsAdvFirewall.java b/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/LocalWindowsAdvFirewall.java
new file mode 100644
index 0000000000..93d88d5b6c
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/LocalWindowsAdvFirewall.java
@@ -0,0 +1,109 @@
+package com.ghostchu.peerbanhelper.firewall.impl;
+
+import com.ghostchu.peerbanhelper.PeerBanHelperServer;
+import com.ghostchu.peerbanhelper.firewall.Firewall;
+import inet.ipaddr.IPAddress;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.StringJoiner;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+public class LocalWindowsAdvFirewall extends CommandBasedImpl implements Firewall {
+ private static final String PBH_GUID = new UUID(7355608L, 1145141919810L).toString();
+ private static final String CMD_TEST_START = """
+ New-NetFirewallRule -Id "peerbanhelper-test" -DisplayName "PeerBanHelperTest"
+ """;
+ private static final String CMD_TEST_END = """
+ New-NetFirewallRule -Id "peerbanhelper-test" -DisplayName "PeerBanHelperTest"
+ """;
+ private static final String CMD_REMOVE_FIREWALL_RULE = """
+ Remove-NetFirewallDynamicKeywordAddress -Id {%s}
+ """;
+ private static final String CMD_NEW_INBOUND_FIREWALL_RULE = """
+ New-NetFirewallRule -Id "peerbanhelper-inbound" -DisplayName "PeerBanHelper AdvFirewall Filter (Inbound)" -Direction Inbound -Action Block -RemoteDynamicKeywordAddresses "{%s}"
+ """;
+ private static final String CMD_REMOVE_INBOUND_FIREWALL_RULE = """
+ Remove-NetFirewallRule -Id "peerbanhelper-inbound"
+ """;
+ private static final String CMD_NEW_OUTBOUND_FIREWALL_RULE = """
+ New-NetFirewallRule Id "peerbanhelper-outbound" -DisplayName “PeerBanHelper AdvFirewall Filter (Outbound)” -Direction Outbound -Action Block -RemoteDynamicKeywordAddresses "{%s}"
+ """;
+ private static final String CMD_REMOVE_OUTBOUND_FIREWALL_RULE = """
+ Remove-NetFirewallRule -Id "peerbanhelper-outbound"
+ """;
+ private static final String CMD_CREATE_DYNAMIC_FIREWALL_KEYWORD = """
+ New-NetFirewallDynamicKeywordAddress -Id "{%s}" -Keyword "PBH-BanList" -Addresses "%s"
+ """;
+ private static final String CMD_DELETE_DYNAMIC_FIREWALL_KEYWORD = """
+ Remove-NetFirewallDynamicKeywordAddress -Id "{%s}"
+ """;
+ private final PeerBanHelperServer server;
+
+
+ public LocalWindowsAdvFirewall(PeerBanHelperServer server) {
+ this.server = server;
+ }
+
+ @Override
+ public String getName() {
+ return "Windows AdvFirewall (DynamicKeywordAddress)";
+ }
+
+ @Override
+ public boolean isApplicable() {
+ try {
+ invokeCommand(CMD_TEST_START, Collections.emptyMap());
+ invokeCommand(CMD_TEST_END, Collections.emptyMap());
+ } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean ban(IPAddress address) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ invokeCommand(String.format(CMD_DELETE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID), Collections.emptyMap());
+ invokeCommand(String.format(CMD_CREATE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID, getAllAddress()), Collections.emptyMap());
+ return true;
+ }
+
+ @Override
+ public boolean unban(IPAddress address) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ invokeCommand(String.format(CMD_DELETE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID), Collections.emptyMap());
+ invokeCommand(String.format(CMD_CREATE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID, getAllAddress()), Collections.emptyMap());
+ return true;
+ }
+
+ @Override
+ public boolean reset() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ invokeCommand(String.format(CMD_DELETE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID), Collections.emptyMap());
+ invokeCommand(CMD_REMOVE_INBOUND_FIREWALL_RULE, Collections.emptyMap());
+ invokeCommand(CMD_REMOVE_OUTBOUND_FIREWALL_RULE, Collections.emptyMap());
+ invokeCommand(String.format(CMD_NEW_INBOUND_FIREWALL_RULE, PBH_GUID), Collections.emptyMap());
+ invokeCommand(String.format(CMD_NEW_OUTBOUND_FIREWALL_RULE, PBH_GUID), Collections.emptyMap());
+ invokeCommand(String.format(CMD_CREATE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID, getAllAddress()), Collections.emptyMap());
+ return true;
+ }
+
+ @Override
+ public boolean load() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ return reset();
+ }
+
+ @Override
+ public boolean unload() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ invokeCommand(String.format(CMD_DELETE_DYNAMIC_FIREWALL_KEYWORD, PBH_GUID), Collections.emptyMap());
+ invokeCommand(CMD_REMOVE_OUTBOUND_FIREWALL_RULE, Collections.emptyMap());
+ invokeCommand(CMD_REMOVE_INBOUND_FIREWALL_RULE, Collections.emptyMap());
+ return true;
+ }
+
+ private String getAllAddress() {
+ StringJoiner joiner = new StringJoiner(",");
+ server.getBannedPeers().keySet().forEach(pa -> joiner.add(pa.getAddress().toString()));
+ return joiner.toString();
+ }
+}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java b/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java
index 046d4085f4..1df6cf01c1 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java
@@ -9,11 +9,10 @@
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.StringTokenizer;
+import java.util.*;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
@Slf4j
public class CommandExec implements BanListInvoker {
@@ -40,7 +39,11 @@ public void reset() {
return;
}
for (String c : this.resetCommands) {
- invokeCommand(c, new HashMap<>(0));
+ try {
+ invokeCommand(c, Collections.emptyMap());
+ } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
+ log.warn(Lang.COMMAND_EXECUTOR_FAILED, e);
+ }
}
}
@@ -54,7 +57,11 @@ public void add(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata) {
for (Map.Entry e : map.entrySet()) {
c = c.replace("{%" + e.getKey() + "%}", e.getValue());
}
- invokeCommand(c, map);
+ try {
+ invokeCommand(c, map);
+ } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
+ log.warn(Lang.COMMAND_EXECUTOR_FAILED, e);
+ }
}
}
@@ -68,8 +75,32 @@ public void remove(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata)
for (Map.Entry e : map.entrySet()) {
c = c.replace("{%" + e.getKey() + "%}", e.getValue());
}
- invokeCommand(c, map);
+ try {
+ invokeCommand(c, map);
+ } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
+ log.warn(Lang.COMMAND_EXECUTOR_FAILED, e);
+ }
+ }
+ }
+
+ public int invokeCommand(String command, Map env) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ StringTokenizer st = new StringTokenizer(command);
+ String[] cmdarray = new String[st.countTokens()];
+ for (int i = 0; st.hasMoreTokens(); i++) {
+ cmdarray[i] = st.nextToken();
+ }
+ ProcessBuilder builder = new ProcessBuilder(cmdarray)
+ .inheritIO();
+ Map liveEnv = builder.environment();
+ liveEnv.putAll(env);
+ Process p = builder.start();
+ Process process = p.onExit().get(10, TimeUnit.SECONDS);
+ if (process.isAlive()) {
+ process.destroy();
+ log.warn(Lang.COMMAND_EXECUTOR_FAILED_TIMEOUT, command);
+ return -9999;
}
+ return process.exitValue();
}
private Map makeMap(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata) {
@@ -93,28 +124,4 @@ private Map makeMap(@NotNull PeerAddress peer, @NotNull BanMetad
}
- private void invokeCommand(String command, Map env) {
- StringTokenizer st = new StringTokenizer(command);
- String[] cmdarray = new String[st.countTokens()];
- for (int i = 0; st.hasMoreTokens(); i++)
- cmdarray[i] = st.nextToken();
- try {
- ProcessBuilder builder = new ProcessBuilder(cmdarray)
- .inheritIO();
- Map liveEnv = builder.environment();
- liveEnv.putAll(env);
- Process p = builder.start();
- p.waitFor(5, TimeUnit.SECONDS);
- if (p.isAlive()) {
- log.info(Lang.BANLIST_INVOKER_COMMAND_EXEC_TIMEOUT, command);
- }
- p.onExit().thenAccept(process -> {
- if (process.exitValue() != 0) {
- log.warn(Lang.BANLIST_INVOKER_COMMAND_EXEC_FAILED, command, process.exitValue());
- }
- });
- } catch (IOException | InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java
index f64a8cf5db..fd2ee6f710 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java
@@ -126,9 +126,14 @@ private void handleDownloaderTest(Context ctx) {
ctx.json(Map.of("message", Lang.DOWNLOADER_API_ADD_FAILURE));
return;
}
- boolean testResult = downloader.login();
- ctx.status(HttpStatus.OK);
- ctx.json(Map.of("message", Lang.DOWNLOADER_API_TEST_OK, "valid", testResult));
+ try {
+ boolean testResult = downloader.login();
+ ctx.status(HttpStatus.OK);
+ ctx.json(Map.of("message", Lang.DOWNLOADER_API_TEST_OK, "valid", testResult));
+ } catch (Exception e) {
+ ctx.status(HttpStatus.INTERNAL_SERVER_ERROR);
+ ctx.json(Map.of("message", e.getMessage(), "valid", false));
+ }
}
private void handleDownloaderDelete(Context ctx, String downloaderName) {
diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java b/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java
index 99760ac5d3..8fcc1cf0b9 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java
@@ -269,4 +269,10 @@ public class Lang {
public static final String DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED = "[错误] 向下载器请求增量封禁对等体时出现错误,请尝试在配置文件中关闭增量封禁(increment-ban)配置项";
public static final String DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST = "无法保存 {} ({}) 的 Banlist!{} - {}\n{}";
public static final String ALERT_INCORRECT_PROXY_SETTING = "警告!通过 HTTP_PROXY 环境变量无法为 Java 应用程序设置代理服务器!您的代理设置可能并不会生效。";
+ public static final String COMMAND_EXECUTOR = "[CommandExecutor] 命令执行器正在执行系统终端命令:{}";
+ public static final String COMMAND_EXECUTOR_FAILED = "[CommandExecutor] 系统终端命令执行失败:{}";
+ public static final String COMMAND_EXECUTOR_FAILED_TIMEOUT = "[CommandExecutor] 系统终端命令执行超时:{}";
+ public static final String DOWNLOADER_DELUGE_PLUGIN_NOT_INSTALLED = "无法登录到下载器 {},此 Deluge 下载器必须正确加载 PeerBanHelper Deluge Adapter 扩展插件:https://github.com/PBH-BTN/PBH-Adapter-Deluge";
+ public static final String DOWNLOADER_DELUGE_API_ERROR = "执行 Deluge RPC 调用失败,操作被忽略";
+ public static final String DOWNLOADER_UNHANDLED_EXCEPTION = "发生了一个未处理的异常,请反馈给 PeerBanHelper 开发者,此错误已被跳过……";
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/time/RestrictedExecutor.java b/src/main/java/com/ghostchu/peerbanhelper/util/time/RestrictedExecutor.java
index 29fee39091..49fd4752cc 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/util/time/RestrictedExecutor.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/util/time/RestrictedExecutor.java
@@ -18,6 +18,7 @@ public static RestrictedExecResult execute(long timeout, Supplier targ
return new RestrictedExecResult<>(true, null);
} catch (InterruptedException e) {
log.warn("Thread Interrupted", e);
+ Thread.currentThread().interrupt();
return new RestrictedExecResult<>(false, null);
} catch (ExecutionException e) {
throw new RuntimeException(e);
diff --git a/src/main/java/raccoonfink/deluge/DelugeEvent.java b/src/main/java/raccoonfink/deluge/DelugeEvent.java
new file mode 100644
index 0000000000..6b715ef105
--- /dev/null
+++ b/src/main/java/raccoonfink/deluge/DelugeEvent.java
@@ -0,0 +1,15 @@
+package raccoonfink.deluge;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class DelugeEvent {
+
+ public DelugeEvent(final JSONArray data) {
+ }
+
+ public JSONObject toJSON() {
+ return new JSONObject();
+ }
+
+}
diff --git a/src/main/java/raccoonfink/deluge/DelugeException.java b/src/main/java/raccoonfink/deluge/DelugeException.java
new file mode 100644
index 0000000000..7e831c651e
--- /dev/null
+++ b/src/main/java/raccoonfink/deluge/DelugeException.java
@@ -0,0 +1,21 @@
+package raccoonfink.deluge;
+
+public class DelugeException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public DelugeException() {
+ }
+
+ public DelugeException(final String message) {
+ super(message);
+ }
+
+ public DelugeException(final Throwable cause) {
+ super(cause);
+ }
+
+ public DelugeException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/src/main/java/raccoonfink/deluge/DelugeRequest.java b/src/main/java/raccoonfink/deluge/DelugeRequest.java
new file mode 100644
index 0000000000..2d3dd449c3
--- /dev/null
+++ b/src/main/java/raccoonfink/deluge/DelugeRequest.java
@@ -0,0 +1,27 @@
+package raccoonfink.deluge;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class DelugeRequest {
+ private final String m_method;
+ private final List