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 m_params; + + public DelugeRequest(final String method, final Object... params) { + m_method = method; + m_params = Arrays.asList(params); + } + + public String toPostData(final int id) throws JSONException { + assert (id >= 0); + final JSONObject json = new JSONObject(); + json.put("id", id); + json.put("method", m_method); + json.put("params", new JSONArray(m_params)); + return json.toString(); + } +} diff --git a/src/main/java/raccoonfink/deluge/DelugeServer.java b/src/main/java/raccoonfink/deluge/DelugeServer.java new file mode 100644 index 0000000000..7b9563c270 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/DelugeServer.java @@ -0,0 +1,306 @@ +package raccoonfink.deluge; + +import com.ghostchu.peerbanhelper.Main; +import com.ghostchu.peerbanhelper.util.HTTPUtil; +import com.github.mizosoft.methanol.Methanol; +import com.github.mizosoft.methanol.MutableRequest; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raccoonfink.deluge.responses.*; + +import java.io.IOException; +import java.net.*; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; + +public class DelugeServer { + private static final Logger log = LoggerFactory.getLogger(DelugeServer.class); + private final String m_url; + private final String m_password; + private final HttpClient httpClient; + + private final CookieManager m_cookieManager = new CookieManager(); + private int m_counter = 0; + + public DelugeServer(final String url, final String password, boolean verifySSL, HttpClient.Version httpVersion, String baUser, String baPassword) { + m_url = url; + m_password = password; + m_cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + + HttpURLConnection.setFollowRedirects(true); + + HttpClient.Builder builder = Methanol + .newBuilder() + .version(httpVersion) + .followRedirects(HttpClient.Redirect.ALWAYS) + .userAgent(Main.getUserAgent()) + .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) + .headersTimeout(Duration.of(10, ChronoUnit.SECONDS)) + .readTimeout(Duration.of(15, ChronoUnit.SECONDS)) + .requestTimeout(Duration.of(15, ChronoUnit.SECONDS)) + .defaultHeader("Accept", "application/json") + .defaultHeader("Accept-Encoding", "compress;q=0.5, gzip;q=1.0") + .defaultHeader("Content-Type", "application/json") + .authenticator(new Authenticator() { + @Override + public PasswordAuthentication requestPasswordAuthenticationInstance(String host, InetAddress addr, int port, String protocol, String prompt, String scheme, URL url, RequestorType reqType) { + return new PasswordAuthentication(baUser, baPassword.toCharArray()); + } + }) + .cookieHandler(m_cookieManager); + if (!verifySSL && HTTPUtil.getIgnoreSslContext() != null) { + builder = builder.sslContext(HTTPUtil.getIgnoreSslContext()); + } + this.httpClient = builder.build(); + } + +// private static void closeQuietly(final Closeable c) { +// if (c == null) { +// return; +// } +// try { +// c.close(); +// } catch (final IOException e) { +// } +// } + + public DelugeResponse makeRequest(final DelugeRequest delugeRequest) throws DelugeException { + int connectionResponseCode; + JSONObject jsonResponse; + + final String postData = delugeRequest.toPostData(m_counter++); + //final String cookieHeader = getCookieHeader(); + HttpResponse resp; + try { + if (postData != null) { + resp = httpClient + .send(MutableRequest.POST(m_url, HttpRequest.BodyPublishers.ofString(postData)), + //.header("Cookie", cookieHeader) + //.header("Content-Length", String.valueOf(postData.getBytes(StandardCharsets.UTF_8).length)), + java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } else { + resp = httpClient + .send(MutableRequest.POST(m_url, HttpRequest.BodyPublishers.noBody()), + //.header("Cookie", cookieHeader), + java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } + connectionResponseCode = resp.statusCode(); + if (connectionResponseCode != 200) { + throw new DelugeException(resp.statusCode() + " - " + resp.body()); + } + jsonResponse = new JSONObject(resp.body()); +// List setCookie = resp.headers().allValues("Set-Cookie"); +// if(!setCookie.isEmpty()){ +// addCookies(setCookie); +// } + } catch (final IOException | JSONException e) { + throw new DelugeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new DelugeException(e); + } + try { + if (jsonResponse.has("error") && !jsonResponse.isNull("error")) { + final JSONObject error = jsonResponse.getJSONObject("error"); + final String message = error.optString("message"); + final int code = error.optInt("code"); + final StringBuilder builder = new StringBuilder("Error"); + if (code >= 0) { + builder.append(" ").append(code); + } + if (message != null) { + builder.append(": ").append(message); + } + throw new DelugeException(builder.toString()); + } + } catch (final JSONException e) { + throw new DelugeException(e); + } + return new DelugeResponse(connectionResponseCode, jsonResponse); + } + +// private String getCookieHeader() { +// StringJoiner joiner = new StringJoiner(","); +// final List cookies = m_cookieManager.getCookieStore().getCookies(); +// if (cookies == null) { +// return ""; +// } +// for (HttpCookie cookie : cookies) { +// joiner.add(cookie.toString()); +// } +// return joiner.toString(); +// } + +// private void addCookies(final List cookies) { +// if (cookies == null) { +// return; +// } +// for (final String cookie : cookies) { +// m_cookieManager.getCookieStore().add(null, HttpCookie.parse(cookie).get(0)); +// } +// } + + public DelugeListMethodsResponse listMethods() throws DelugeException { + DelugeResponse response = makeRequest(new DelugeRequest("system.listMethods")); + return new DelugeListMethodsResponse(response.getResponseCode(), response.getResponseData()); + } + + public CheckSessionResponse checkSession() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("auth.check_session")); + return new CheckSessionResponse(response.getResponseCode(), response.getResponseData()); + } + + public LoginResponse login() throws DelugeException { + final DelugeRequest request = new DelugeRequest("auth.login", m_password); + final DelugeResponse response = makeRequest(request); + return new LoginResponse(response.getResponseCode(), response.getResponseData()); + } + + public DeleteSessionResponse deleteSession() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("auth.delete_session")); + return new DeleteSessionResponse(response.getResponseCode(), response.getResponseData()); + } + + public void registerEventListeners() throws DelugeException { + final List events = Arrays.asList("ConfigValueChangedEvent", + "NewVersionAvailableEvent", + "PluginDisabledEvent", + "PluginEnabledEvent", + "PreTorrentRemovedEvent", + "SessionPausedEvent", + "SessionResumedEvent", + "SessionStartedEvent", + "TorrentAddedEvent", + "TorrentFileRenamedEvent", + "TorrentFinishedEvent", + "TorrentFolderRenamedEvent", + "TorrentQueueChangedEvent", + "TorrentRemovedEvent", + "TorrentResumedEvent", + "TorrentStateChangedEvent"); + + for (final String event : events) { + makeRequest(new DelugeRequest("web.register_event_listener", event)); + } + } + + public ConnectedResponse isConnected() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.connected")); + return new ConnectedResponse(response.getResponseCode(), response.getResponseData()); + } + + public HostResponse getHosts() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.get_hosts")); + return new HostResponse(response.getResponseCode(), response.getResponseData(), false); + } + + public HostResponse getHostStatus(final String id) throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.get_host_status", id)); + return new HostResponse(response.getResponseCode(), response.getResponseData(), true); + } + + public ConnectedResponse connect(final String id) throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.connect", id)); + if (response.getResponseData().isNull("result")) { + return new ConnectedResponse(response.getResponseCode(), response.getResponseData(), true); + } else { + return new ConnectedResponse(response.getResponseCode(), response.getResponseData()); + } + } + + public ConnectedResponse disconnect() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.disconnect")); + if (response.getResponseData().isNull("result")) { + return new ConnectedResponse(response.getResponseCode(), response.getResponseData(), false); + } else { + return new ConnectedResponse(response.getResponseCode(), response.getResponseData(), true); + } + } + + public EventsResponse getEvents() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.get_events")); + return new EventsResponse(response.getResponseCode(), response.getResponseData()); + } + + public UIResponse updateUI() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("web.update_ui", new JSONArray(Arrays.asList( + "queue", + "name", + "total_size", + "state", + "progress", + "num_seeds", + "total_seeds", + "num_peers", + "total_peers", + "download_payload_rate", + "upload_payload_rate", + "eta", + "ratio", + "distributed_copies", + "is_auto_managed", + "time_added", + "tracker_host", + "save_path", + "total_done", + "total_uploaded", + "max_download_speed", + "max_upload_speed", + "seeds_peers_ratio" + )), new JSONObject())); + return new UIResponse(response.getResponseCode(), response.getResponseData()); + } + + public PBHActiveTorrentsResponse getActiveTorrents() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("peerbanhelperadapter.get_active_torrents_info")); + return new PBHActiveTorrentsResponse(response.getResponseCode(), response.getResponseData()); + } + + public PBHBannedPeersResponse getBannedPeers() throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("peerbanhelperadapter.get_blocklist")); + return new PBHBannedPeersResponse(response.getResponseCode(), response.getResponseData()); + } + + public boolean banPeers(List ips) throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("peerbanhelperadapter.ban_ips", ips)); + return !determineResponseError(response); + } + + public boolean unbanPeers(List ips) throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("peerbanhelperadapter.unban_ips", ips)); + return !determineResponseError(response); + } + + public boolean replaceBannedPeers(List ips) throws DelugeException { + final DelugeResponse response = makeRequest(new DelugeRequest("peerbanhelperadapter.replace_blocklist", ips)); + return !determineResponseError(response); + } + + private boolean determineResponseError(DelugeResponse response) { + if (response.getResponseData().isNull("error")) { + return false; + } + Object error = response.getResponseData().get("error"); + if (error instanceof Boolean && !(Boolean) error) { + return false; + } + printError(response); + return true; + } + + private void printError(DelugeResponse response) { + System.out.println(response.getResponseData().toString()); + JSONObject object = response.getResponseData().getJSONObject("error"); + log.info("Error when call Deluge RPC: message={}, code={}", object.getString("message"), object.getInt("code")); + } +} + diff --git a/src/main/java/raccoonfink/deluge/Host.java b/src/main/java/raccoonfink/deluge/Host.java new file mode 100644 index 0000000000..f4743c3a87 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/Host.java @@ -0,0 +1,54 @@ +package raccoonfink.deluge; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Host { + private final String m_id; + private final String m_hostname; + private final int m_port; + private final Status m_status; + private final String m_version; + public Host(final String id, final String hostname, final int port, final String status, final String version) { + m_id = id; + m_hostname = hostname; + m_port = port; + m_status = Status.valueOf(status); + m_version = version; + } + + public String getId() { + return m_id; + } + + public String getHostname() { + return m_hostname; + } + + public int getPort() { + return m_port; + } + + public Status getStatus() { + return m_status; + } + + public String getVersion() { + return m_version; + } + + public JSONObject toJSON() throws JSONException { + final JSONObject ret = new JSONObject(); + ret.put("id", m_id); + ret.put("hostname", m_hostname); + ret.put("status", m_status); + ret.putOpt("version", m_version); + return ret; + } + + public static enum Status { + Offline, + Online, + Connected + } +} diff --git a/src/main/java/raccoonfink/deluge/Statistics.java b/src/main/java/raccoonfink/deluge/Statistics.java new file mode 100644 index 0000000000..ba134d8ab6 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/Statistics.java @@ -0,0 +1,93 @@ +package raccoonfink.deluge; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Statistics { + + private int m_dhtNodes; + private int m_downloadProtocolRate; + private int m_downloadRate; + private int m_freeSpace; + private boolean m_incomingConnections; + private double m_maxDownload; + private int m_maxNumConnections; + private double m_maxUpload; + private int m_numConnections; + private int m_uploadProtocolRate; + private int m_uploadRate; + + public Statistics(final JSONObject stats) { + m_dhtNodes = stats.optInt("dht_nodes"); + m_downloadProtocolRate = stats.optInt("download_protocol_rate"); + m_downloadRate = stats.optInt("download_rate"); + m_freeSpace = stats.optInt("free_space"); + m_incomingConnections = stats.optBoolean("has_incoming_connections"); + m_maxDownload = stats.optDouble("max_download"); + m_maxNumConnections = stats.optInt("max_num_connections"); + m_maxUpload = stats.optDouble("max_upload"); + m_numConnections = stats.optInt("num_connections"); + m_uploadProtocolRate = stats.optInt("upload_protocol_rate"); + m_uploadRate = stats.optInt("upload_rate"); + } + + public int getDHTNodes() { + return m_dhtNodes; + } + + public int getDownloadProtocolRate() { + return m_downloadProtocolRate; + } + + public int getDownloadRate() { + return m_downloadRate; + } + + public int getFreeSpace() { + return m_freeSpace; + } + + public boolean hasIncomingConnections() { + return m_incomingConnections; + } + + public double getMaxDownload() { + return m_maxDownload; + } + + public int getMaxNumConnections() { + return m_maxNumConnections; + } + + public double getMaxUpload() { + return m_maxUpload; + } + + public int getNumConnections() { + return m_numConnections; + } + + public int getUploadProtocolRate() { + return m_uploadProtocolRate; + } + + public int getUploadRate() { + return m_uploadRate; + } + + public JSONObject toJSON() throws JSONException { + final JSONObject ret = new JSONObject(); + ret.put("dht_nodes", m_dhtNodes); + ret.put("download_protocol_rate", m_downloadProtocolRate); + ret.put("download_rate", m_downloadRate); + ret.put("free_space", m_freeSpace); + ret.put("incoming_connections", m_incomingConnections); + ret.put("max_download", m_maxDownload); + ret.put("max_num_connections", m_maxNumConnections); + ret.put("max_upload", m_maxUpload); + ret.put("num_connections", m_numConnections); + ret.put("upload_protocol_rate", m_uploadProtocolRate); + ret.put("upload_rate", m_uploadRate); + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/Torrent.java b/src/main/java/raccoonfink/deluge/Torrent.java new file mode 100644 index 0000000000..2d85b32d1c --- /dev/null +++ b/src/main/java/raccoonfink/deluge/Torrent.java @@ -0,0 +1,207 @@ +package raccoonfink.deluge; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Torrent implements Comparable { + private final String m_key; + private double m_distributedCopies; + private long m_downloadPayloadRate; + private long m_eta; + private boolean m_autoManaged; + private long m_maxDownloadSpeed; + private long m_maxUploadSpeed; + private String m_name; + private long m_numPeers; + private long m_numSeeds; + private double m_progress; + private long m_queue; + private double m_ratio; + private String m_savePath; + private double m_seedsPeerRatio; + private double m_timeAdded; + private long m_totalDone; + private long m_totalPeers; + private long m_totalSeeds; + private long m_totalSize; + private long m_totalUploaded; + private String m_trackerHost; + private long m_uploadPayloadRate; + private State m_state; + public Torrent(final String key, final JSONObject data) { + m_key = key; + m_distributedCopies = data.optDouble("distributed_copies"); + m_downloadPayloadRate = data.optLong("download_payload_rate"); + m_eta = data.optLong("eta"); + m_autoManaged = data.optBoolean("is_auto_managed"); + m_maxDownloadSpeed = data.optLong("max_download_speed"); + m_maxUploadSpeed = data.optLong("max_upload_speed"); + m_name = data.optString("name"); + m_numPeers = data.optLong("num_peers"); + m_numSeeds = data.optLong("num_seeds"); + m_progress = data.optDouble("progress"); + m_queue = data.optLong("queue"); + m_ratio = data.optDouble("ratio"); + m_savePath = data.optString("save_path"); + m_seedsPeerRatio = data.optDouble("seeds_peer_ratio"); + m_timeAdded = data.optDouble("time_added"); + m_totalDone = data.optLong("total_done"); + m_totalPeers = data.optLong("total_peers"); + m_totalSeeds = data.optLong("total_seeds"); + m_totalSize = data.optLong("total_size"); + m_totalUploaded = data.optLong("total_uploaded"); + m_trackerHost = data.optString("tracker_host"); + m_uploadPayloadRate = data.optLong("upload_payload_rate"); + + final String state = data.optString("state"); + if ("Downloading Metadata".equals(state)) { + m_state = State.Downloading_Metadata; + } else if ("Checking Resume Data".equals(state)) { + m_state = State.Checking_Resume_Data; + } else { + m_state = State.valueOf(state); + } + } + + public String getKey() { + return m_key; + } + + public double getDistributedCopies() { + return m_distributedCopies; + } + + public long getDownloadPayloadRate() { + return m_downloadPayloadRate; + } + + public long getEta() { + return m_eta; + } + + public boolean isAutoManaged() { + return m_autoManaged; + } + + public long getMaxDownloadSpeed() { + return m_maxDownloadSpeed; + } + + public long getMaxUploadSpeed() { + return m_maxUploadSpeed; + } + + public String getName() { + return m_name; + } + + public long getNumPeers() { + return m_numPeers; + } + + public long getNumSeeds() { + return m_numSeeds; + } + + public double getProgress() { + return m_progress; + } + + public long getQueue() { + return m_queue; + } + + public double getRatio() { + return m_ratio; + } + + public String getSavePath() { + return m_savePath; + } + + public double getSeedsPeerRatio() { + return m_seedsPeerRatio; + } + + public double getTimeAdded() { + return m_timeAdded; + } + + public long getTotalDone() { + return m_totalDone; + } + + public long getTotalPeers() { + return m_totalPeers; + } + + public long getTotalSeeds() { + return m_totalSeeds; + } + + public long getTotalSize() { + return m_totalSize; + } + + public long getTotalUploaded() { + return m_totalUploaded; + } + + public String getTrackerHost() { + return m_trackerHost; + } + + public long getUploadPayloadRate() { + return m_uploadPayloadRate; + } + + public State getState() { + return m_state; + } + + public int compareTo(final Torrent torrent) { + return this.getName().compareTo(torrent.getName()); + } + + public JSONObject toJSON() throws JSONException { + final JSONObject ret = new JSONObject(); + ret.put("key", m_key); + ret.put("distributed_copies", m_distributedCopies); + ret.put("download_payload_rate", m_downloadPayloadRate); + ret.put("eta", m_eta); + ret.put("auto_managed", m_autoManaged); + ret.put("max_download_speed", m_maxDownloadSpeed); + ret.put("max_upload_speed", m_maxUploadSpeed); + ret.put("name", m_name); + ret.put("num_peers", m_numPeers); + ret.put("num_seeds", m_numSeeds); + ret.put("progress", m_progress); + ret.put("queue", m_queue); + ret.put("ratio", m_ratio); + ret.put("save_path", m_savePath); + ret.put("seeds_peer_ratio", m_seedsPeerRatio); + ret.put("time_added", m_timeAdded); + ret.put("total_done", m_totalDone); + ret.put("total_peers", m_totalPeers); + ret.put("total_seeds", m_totalSeeds); + ret.put("total_size", m_totalSize); + ret.put("total_uploaded", m_totalUploaded); + ret.put("tracker_host", m_trackerHost); + ret.put("upload_payload_rate", m_uploadPayloadRate); + ret.put("state", m_state); + return ret; + } + + public static enum State { + Queued, + Checking, + Downloading_Metadata, + Downloading, + Finished, + Seeding, + Allocating, + Checking_Resume_Data + } + +} + diff --git a/src/main/java/raccoonfink/deluge/responses/CheckSessionResponse.java b/src/main/java/raccoonfink/deluge/responses/CheckSessionResponse.java new file mode 100644 index 0000000000..09fb941010 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/CheckSessionResponse.java @@ -0,0 +1,30 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +public class CheckSessionResponse extends DelugeResponse { + private final boolean m_sessionActive; + + public CheckSessionResponse(final Integer httpResponseCode, final JSONObject result) throws DelugeException { + super(httpResponseCode, result); + + if (result != null) { + m_sessionActive = result.optBoolean("result"); + } else { + m_sessionActive = false; + } + } + + public boolean isSessionActive() { + return m_sessionActive; + } + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + ret.put("result", isSessionActive()); + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/ConnectedResponse.java b/src/main/java/raccoonfink/deluge/responses/ConnectedResponse.java new file mode 100644 index 0000000000..279b61b9a8 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/ConnectedResponse.java @@ -0,0 +1,35 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +public class ConnectedResponse extends DelugeResponse { + private final boolean m_connected; + + public ConnectedResponse(final Integer httpResponseCode, final JSONObject response) throws DelugeException { + super(httpResponseCode, response); + try { + m_connected = response.getBoolean("result"); + } catch (final JSONException e) { + throw new DelugeException(e); + } + } + + public ConnectedResponse(final Integer httpResponseCode, final JSONObject response, final boolean isConnected) throws DelugeException { + super(httpResponseCode, response); + m_connected = isConnected; + } + + public boolean isConnected() { + return m_connected; + } + + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + ret.put("result", isConnected()); + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/DeleteSessionResponse.java b/src/main/java/raccoonfink/deluge/responses/DeleteSessionResponse.java new file mode 100644 index 0000000000..9696d09d7e --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/DeleteSessionResponse.java @@ -0,0 +1,30 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +public class DeleteSessionResponse extends DelugeResponse { + private final boolean m_sessionDeleted; + + public DeleteSessionResponse(final Integer httpResponseCode, final JSONObject result) throws DelugeException { + super(httpResponseCode, result); + + if (result != null) { + m_sessionDeleted = result.optBoolean("result"); + } else { + m_sessionDeleted = false; + } + } + + public boolean isSessionDeleted() { + return m_sessionDeleted; + } + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + ret.put("result", isSessionDeleted()); + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/DelugeListMethodsResponse.java b/src/main/java/raccoonfink/deluge/responses/DelugeListMethodsResponse.java new file mode 100644 index 0000000000..1c27e66e49 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/DelugeListMethodsResponse.java @@ -0,0 +1,22 @@ +package raccoonfink.deluge.responses; + +import lombok.Getter; +import org.json.JSONArray; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class DelugeListMethodsResponse extends DelugeResponse { + private final List delugeSupportedMethods = new ArrayList<>(); + + public DelugeListMethodsResponse(Integer httpResponseCode, JSONObject response) throws DelugeException { + super(httpResponseCode, response); + JSONArray jsonArray = response.getJSONArray("result"); + jsonArray.forEach(object -> { + delugeSupportedMethods.add((String) object); + }); + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/DelugeResponse.java b/src/main/java/raccoonfink/deluge/responses/DelugeResponse.java new file mode 100644 index 0000000000..e29c93f306 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/DelugeResponse.java @@ -0,0 +1,44 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +public class DelugeResponse { + + private final int m_id; + private final int m_responseCode; + private final JSONObject m_result; + + public DelugeResponse(final Integer httpResponseCode, final JSONObject response) throws DelugeException { + assert (httpResponseCode != null); + + try { + m_id = response.getInt("id"); + } catch (JSONException e) { + throw new DelugeException("Invalid 'id' field in JSON: " + response.toString(), e); + } + m_responseCode = httpResponseCode.intValue(); + m_result = response; + } + + public int getId() { + return m_id; + } + + public int getResponseCode() { + return m_responseCode; + } + + public JSONObject getResponseData() { + return m_result; + } + + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = new JSONObject(); + ret.put("id", getId()); + ret.put("responseCode", getResponseCode()); + ret.put("result", JSONObject.NULL); + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/EventsResponse.java b/src/main/java/raccoonfink/deluge/responses/EventsResponse.java new file mode 100644 index 0000000000..4638021166 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/EventsResponse.java @@ -0,0 +1,43 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeEvent; +import raccoonfink.deluge.DelugeException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class EventsResponse extends DelugeResponse { + private final List m_events = new ArrayList(); + + public EventsResponse(final Integer httpResponseCode, final JSONObject result) throws DelugeException { + super(httpResponseCode, result); + + if (!result.isNull("result")) { + try { + final JSONArray res = result.getJSONArray("result"); + for (int i = 0; i < res.length(); i++) { + m_events.add(new DelugeEvent(res.getJSONArray(i))); + } + } catch (final JSONException e) { + throw new DelugeException(e); + } + } + } + + public List getEvents() { + return Collections.unmodifiableList(m_events); + } + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + for (final DelugeEvent ev : m_events) { + ret.append("result", ev.toJSON()); + } + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/HostResponse.java b/src/main/java/raccoonfink/deluge/responses/HostResponse.java new file mode 100644 index 0000000000..d5fc20f884 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/HostResponse.java @@ -0,0 +1,49 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; +import raccoonfink.deluge.Host; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class HostResponse extends DelugeResponse { + private final List m_hosts = new ArrayList(); + + public HostResponse(final Integer httpResponseCode, final JSONObject response, final boolean singleResult) throws DelugeException { + super(httpResponseCode, response); + try { + final JSONArray result = response.getJSONArray("result"); + if (singleResult) { + m_hosts.add(getHost(result)); + } else { + for (int i = 0; i < result.length(); i++) { + final JSONArray host = result.getJSONArray(i); + m_hosts.add(getHost(host)); + } + } + } catch (final JSONException e) { + throw new DelugeException(e); + } + } + + public List getHosts() { + return Collections.unmodifiableList(m_hosts); + } + + private Host getHost(final JSONArray host) throws JSONException { + return new Host(host.getString(0), host.getString(1), host.getInt(2), host.getString(3), host.optString(4)); + } + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + for (final Host host : m_hosts) { + ret.append("result", host.toJSON()); + } + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/LoginResponse.java b/src/main/java/raccoonfink/deluge/responses/LoginResponse.java new file mode 100644 index 0000000000..9a63b033d9 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/LoginResponse.java @@ -0,0 +1,30 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +public class LoginResponse extends DelugeResponse { + private final boolean m_loggedIn; + + public LoginResponse(final Integer httpResponseCode, final JSONObject response) throws DelugeException { + super(httpResponseCode, response); + + if (response != null) { + m_loggedIn = response.optBoolean("result"); + } else { + m_loggedIn = false; + } + } + + public boolean isLoggedIn() { + return m_loggedIn; + } + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + ret.put("result", isLoggedIn()); + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/PBHActiveTorrentsResponse.java b/src/main/java/raccoonfink/deluge/responses/PBHActiveTorrentsResponse.java new file mode 100644 index 0000000000..6d0995fec4 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/PBHActiveTorrentsResponse.java @@ -0,0 +1,120 @@ +package raccoonfink.deluge.responses; + +import com.ghostchu.peerbanhelper.util.JsonUtil; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.json.JSONArray; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +import java.util.List; + +@Getter +public class PBHActiveTorrentsResponse extends DelugeResponse { + private List activeTorrents; + + public PBHActiveTorrentsResponse(final Integer httpResponseCode, final JSONObject response) throws DelugeException { + super(httpResponseCode, response); + if (response.isNull("result")) { + return; + } + JSONArray jsonArray = response.getJSONArray("result"); + String resultJson = jsonArray.toString(); + this.activeTorrents = JsonUtil.getGson().fromJson(resultJson, new TypeToken>() { + }.getType()); + } + + @NoArgsConstructor + @Data + public static class ActiveTorrentsResponseDTO { + + @SerializedName("id") + private String id; + @SerializedName("name") + private String name; + @SerializedName("info_hash") + private String infoHash; + @SerializedName("progress") + private Double progress; + @SerializedName("size") + private Long size; + @SerializedName("upload_payload_rate") + private Integer uploadPayloadRate; + @SerializedName("download_payload_rate") + private Integer downloadPayloadRate; + @SerializedName("peers") + private List peers; + + @NoArgsConstructor + @Data + public static class PeersDTO { + @SerializedName("ip") + private String ip; + @SerializedName("port") + private Integer port; + @SerializedName("peer_id") + private String peerId; + @SerializedName("client_name") + private String clientName; + @SerializedName("up_speed") + private Integer upSpeed; + @SerializedName("down_speed") + private Integer downSpeed; + @SerializedName("payload_up_speed") + private Integer payloadUpSpeed; + @SerializedName("payload_down_speed") + private Integer payloadDownSpeed; + @SerializedName("total_upload") + private Integer totalUpload; + @SerializedName("total_download") + private Integer totalDownload; + @SerializedName("progress") + private Double progress; + @SerializedName("flags") + private Integer flags; + @SerializedName("source") + private Integer source; + @SerializedName("local_endpoint_ip") + private String localEndpointIp; + @SerializedName("local_endpoint_port") + private Integer localEndpointPort; + @SerializedName("queue_bytes") + private Integer queueBytes; + @SerializedName("request_timeout") + private Integer requestTimeout; + @SerializedName("num_hashfails") + private Integer numHashfails; + @SerializedName("download_queue_length") + private Integer downloadQueueLength; + @SerializedName("upload_queue_length") + private Integer uploadQueueLength; + @SerializedName("failcount") + private Integer failcount; + @SerializedName("downloading_block_index") + private Integer downloadingBlockIndex; + @SerializedName("downloading_progress") + private Integer downloadingProgress; + @SerializedName("downloading_total") + private Integer downloadingTotal; + @SerializedName("connection_type") + private Integer connectionType; + @SerializedName("send_quota") + private Integer sendQuota; + @SerializedName("receive_quota") + private Integer receiveQuota; + @SerializedName("rtt") + private Integer rtt; + @SerializedName("num_pieces") + private Integer numPieces; + @SerializedName("download_rate_peak") + private Integer downloadRatePeak; + @SerializedName("upload_rate_peak") + private Integer uploadRatePeak; + @SerializedName("progress_ppm") + private Integer progressPpm; + } + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/PBHBannedPeersResponse.java b/src/main/java/raccoonfink/deluge/responses/PBHBannedPeersResponse.java new file mode 100644 index 0000000000..296dc5d34b --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/PBHBannedPeersResponse.java @@ -0,0 +1,35 @@ +package raccoonfink.deluge.responses; + +import com.ghostchu.peerbanhelper.util.JsonUtil; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; + +import java.util.List; + +@Getter +public class PBHBannedPeersResponse extends DelugeResponse { + private BannedPeersResponseDTO bannedPeers; + + public PBHBannedPeersResponse(final Integer httpResponseCode, final JSONObject response) throws DelugeException { + super(httpResponseCode, response); + if (response.isNull("result")) { + return; + } + JSONObject jsonObject = response.getJSONObject("result"); + String resultJson = jsonObject.toString(); + this.bannedPeers = JsonUtil.getGson().fromJson(resultJson, BannedPeersResponseDTO.class); + } + + @NoArgsConstructor + @Data + public static class BannedPeersResponseDTO { + @SerializedName("size") + private Integer size; + @SerializedName("ips") + private List ips; + } +} diff --git a/src/main/java/raccoonfink/deluge/responses/UIResponse.java b/src/main/java/raccoonfink/deluge/responses/UIResponse.java new file mode 100644 index 0000000000..ce9ebe6b50 --- /dev/null +++ b/src/main/java/raccoonfink/deluge/responses/UIResponse.java @@ -0,0 +1,70 @@ +package raccoonfink.deluge.responses; + +import org.json.JSONException; +import org.json.JSONObject; +import raccoonfink.deluge.DelugeException; +import raccoonfink.deluge.Statistics; +import raccoonfink.deluge.Torrent; + +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +public class UIResponse extends DelugeResponse { + private boolean m_connected = false; + private Statistics m_statistics; + private Set m_torrents = new TreeSet(); + + @SuppressWarnings("rawtypes") + public UIResponse(final Integer httpResponseCode, final JSONObject response) throws DelugeException { + super(httpResponseCode, response); + + if (response == null) { + return; + } + + try { + final JSONObject result = response.getJSONObject("result"); + m_connected = result.optBoolean("connected"); + + m_statistics = new Statistics(result.getJSONObject("stats")); + + final JSONObject torrents = result.optJSONObject("torrents"); + if (torrents != null) { + final Iterator it = torrents.keys(); + while (it.hasNext()) { + final String key = (String) it.next(); + m_torrents.add(new Torrent(key, torrents.getJSONObject(key))); + + } + } + } catch (final JSONException e) { + throw new DelugeException(e); + } + } + + public boolean isConnected() { + return m_connected; + } + + public Statistics getStatistics() { + return m_statistics; + } + + public Set getTorrents() { + return m_torrents; + } + + @Override + public JSONObject toResponseJSON() throws JSONException { + final JSONObject ret = super.toResponseJSON(); + ret.put("connected", m_connected); + ret.put("statistics", m_statistics.toJSON()); + final JSONObject torrents = new JSONObject(); + ret.put("torrents", torrents); + for (final Torrent torrent : m_torrents) { + torrents.put(torrent.getKey(), torrent.toJSON()); + } + return ret; + } +} diff --git a/src/main/java/raccoonfink/deluge/ssl/EmptyKeyRelaxedTrustSSLContext.java b/src/main/java/raccoonfink/deluge/ssl/EmptyKeyRelaxedTrustSSLContext.java new file mode 100644 index 0000000000..188c8aa6ff --- /dev/null +++ b/src/main/java/raccoonfink/deluge/ssl/EmptyKeyRelaxedTrustSSLContext.java @@ -0,0 +1,75 @@ +package raccoonfink.deluge.ssl; + +import javax.net.ssl.*; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public final class EmptyKeyRelaxedTrustSSLContext extends SSLContextSpi { + public static final String ALGORITHM = "EmptyKeyRelaxedTrust"; + + private final SSLContext m_delegate; + + public EmptyKeyRelaxedTrustSSLContext() throws NoSuchAlgorithmException, KeyManagementException { + SSLContext customContext = null; + + // Use a blank list of key managers so no SSL keys will be available + KeyManager[] keyManager = null; + TrustManager[] trustManagers = { + new X509TrustManager() { + public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { + // Perform no checks + } + + public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { + // Perform no checks + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }; + customContext = SSLContext.getInstance("SSL"); + customContext.init(keyManager, trustManagers, new SecureRandom()); + + m_delegate = customContext; + } + + @Override + protected SSLEngine engineCreateSSLEngine() { + return m_delegate.createSSLEngine(); + } + + @Override + protected SSLEngine engineCreateSSLEngine(String arg0, int arg1) { + return m_delegate.createSSLEngine(arg0, arg1); + } + + @Override + protected SSLSessionContext engineGetClientSessionContext() { + return m_delegate.getClientSessionContext(); + } + + @Override + protected SSLSessionContext engineGetServerSessionContext() { + return m_delegate.getServerSessionContext(); + } + + @Override + protected SSLServerSocketFactory engineGetServerSocketFactory() { + return m_delegate.getServerSocketFactory(); + } + + @Override + protected javax.net.ssl.SSLSocketFactory engineGetSocketFactory() { + return m_delegate.getSocketFactory(); + } + + @Override + protected void engineInit(KeyManager[] km, TrustManager[] tm, SecureRandom arg2) throws KeyManagementException { + // Don't do anything, we've already initialized everything in the constructor + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f841f50502..e972deee6f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,4 +1,4 @@ -config-version: 8 +config-version: 9 # 客户端设置 client: # 名字,可以自己起,会在日志中显示,只能由字母数字横线组成,数字不能打头 @@ -135,11 +135,8 @@ ip-database: database-city: 'GeoLite2-City' # GeoIP-City 的数据库名称,默认使用免费版(GeoLite2-ASN),如果您购买了付费的数据库,可以自行替换 database-asn: 'GeoLite2-ASN' +# 系统防火墙集成设定 +# 允许不支持 WebAPI 封禁 Peers 的客户端,通过系统防火墙阻断 IP 地址 firewall-integration: - order: - - "wf" # Windows Firewall - - "ipset" # ipset (Linux) - - "ufw" # ufw (Linux) - - "firewalld" # firewalld (Linux) - - "iptables" # iptables (Linux) - + # 高级 Windows 防火墙(基于动态关键字),需要 Windows 10 或更高版本 + windows-adv-firewall: true