From a5ff05485d2d01ed729d305af96b0fd51da08f00 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:10:33 -0600 Subject: [PATCH 1/4] Expose optional names-only Votifier health snapshot --- README.md | 12 ++ .../vexsoftware/votifier/VotifierPlus.java | 7 + .../votifier/bungee/VotifierPlusBungee.java | 15 ++- .../diagnostic/VotifierDiagnostics.java | 51 +++++++ .../VotifierDiagnosticsSnapshot.java | 88 +++++++++++++ .../velocity/VotifierPlusVelocity.java | 15 ++- .../tests/VotifierDiagnosticsTest.java | 124 ++++++++++++++++++ 7 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java create mode 100644 VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnosticsSnapshot.java create mode 100644 VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java diff --git a/README.md b/README.md index a3011f0..9b7463f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ # VotifierPlus Fork of votifier + +## Optional Network Doctor diagnostics + +VotifierPlus exposes a read-only `getNetworkHealthSnapshot()` method on its +Bukkit, BungeeCord, and Velocity entry points for compatible management +software. The immutable snapshot reports whether the provider is present, the +listener is initialized, whether forwarding state is known, and the names of +enabled forwarding destinations. Destination names are bounded and +deduplicated; hosts, ports, keys, tokens, credentials, and configuration text +are never included. The API is optional and does not participate in vote +receipt, event dispatch, or forwarding, so older VotingPlugin and Control +nodes continue to work without it. diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java index f201380..1d4923d 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java @@ -55,6 +55,8 @@ import com.vexsoftware.votifier.crypto.RSAIO; import com.vexsoftware.votifier.crypto.RSAKeygen; import com.vexsoftware.votifier.crypto.TokenUtil; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnostics; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnosticsSnapshot; import com.vexsoftware.votifier.model.Vote; import com.vexsoftware.votifier.model.VotifierEvent; import com.vexsoftware.votifier.net.ThrottleConfig; @@ -72,6 +74,11 @@ */ public class VotifierPlus extends JavaPlugin { + /** Optional read-only management-plane snapshot; never participates in vote processing. */ + public VotifierDiagnosticsSnapshot getNetworkHealthSnapshot() { + return VotifierDiagnostics.snapshot(voteReceiver); + } + /** The Votifier instance. */ private static VotifierPlus instance; diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java index ef788a3..791ae15 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java @@ -16,9 +16,11 @@ import java.util.Set; import com.vexsoftware.votifier.ForwardServer; -import com.vexsoftware.votifier.crypto.RSAIO; -import com.vexsoftware.votifier.crypto.RSAKeygen; -import com.vexsoftware.votifier.crypto.TokenUtil; +import com.vexsoftware.votifier.crypto.RSAIO; +import com.vexsoftware.votifier.crypto.RSAKeygen; +import com.vexsoftware.votifier.crypto.TokenUtil; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnostics; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnosticsSnapshot; import com.vexsoftware.votifier.model.Vote; import com.vexsoftware.votifier.net.ThrottleConfig; import com.vexsoftware.votifier.net.VoteReceiver; @@ -30,7 +32,12 @@ import net.md_5.bungee.config.Configuration; import net.md_5.bungee.config.ConfigurationProvider; -public class VotifierPlusBungee extends Plugin { +public class VotifierPlusBungee extends Plugin { + + /** Optional read-only management-plane snapshot; never participates in vote processing. */ + public VotifierDiagnosticsSnapshot getNetworkHealthSnapshot() { + return VotifierDiagnostics.snapshot(voteReceiver); + } private VotifierPlusBungee instance; @Getter private VoteReceiver voteReceiver; diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java new file mode 100644 index 0000000..ad9a71f --- /dev/null +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java @@ -0,0 +1,51 @@ +package com.vexsoftware.votifier.diagnostic; + +import java.util.ArrayList; +import java.util.List; + +import com.vexsoftware.votifier.ForwardServer; +import com.vexsoftware.votifier.net.VoteReceiver; + +/** Optional adapter used by management software without a hard dependency. */ +public final class VotifierDiagnostics { + private VotifierDiagnostics() { + } + + /** + * Captures only state already owned by the receiver. A null receiver means + * that listener state is unavailable, rather than falsely healthy. + */ + public static VotifierDiagnosticsSnapshot snapshot(VoteReceiver receiver) { + if (receiver == null) { + return new VotifierDiagnosticsSnapshot(Boolean.TRUE, null, null, null); + } + List destinations = new ArrayList(); + boolean forwardingKnown = true; + try { + for (String name : receiver.getServers()) { + if (name == null || name.trim().isEmpty()) { + // An unrepresentable configured entry means the forwarding inventory is incomplete. + forwardingKnown = false; + continue; + } + ForwardServer server = receiver.getServerData(name); + if (server != null && server.isEnabled()) { + String trimmed = name.trim(); + if (trimmed.length() > VotifierDiagnosticsSnapshot.MAX_NAME_LENGTH + || destinations.size() >= VotifierDiagnosticsSnapshot.MAX_DESTINATIONS) { + // Do not claim complete evidence after the bounded snapshot omitted a risk. + forwardingKnown = false; + continue; + } + destinations.add(trimmed); + } + } + } catch (RuntimeException unavailable) { + forwardingKnown = false; + destinations.clear(); + } + return new VotifierDiagnosticsSnapshot(Boolean.TRUE, + receiver.getServer() != null && !receiver.getServer().isClosed(), + forwardingKnown, destinations); + } +} diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnosticsSnapshot.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnosticsSnapshot.java new file mode 100644 index 0000000..b1070f1 --- /dev/null +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnosticsSnapshot.java @@ -0,0 +1,88 @@ +package com.vexsoftware.votifier.diagnostic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Bounded, read-only Votifier state for optional management-plane diagnostics. + * + *

This type deliberately contains destination names only. It does not expose + * forwarding endpoints, ports, keys, tokens, or configuration text.

+ */ +public final class VotifierDiagnosticsSnapshot { + public static final int MAX_DESTINATIONS = 100; + public static final int MAX_NAME_LENGTH = 80; + + private final Boolean providerPresent; + private final Boolean listenerInitialized; + private final Boolean forwardingKnown; + private final List forwardingDestinations; + + public VotifierDiagnosticsSnapshot(Boolean providerPresent, Boolean listenerInitialized, + Boolean forwardingKnown, Iterable forwardingDestinations) { + this.providerPresent = providerPresent; + this.listenerInitialized = listenerInitialized; + BoundedNames bounded = boundedNames(forwardingDestinations); + this.forwardingKnown = forwardingKnown == null || !Boolean.TRUE.equals(forwardingKnown) + ? forwardingKnown : bounded.complete ? Boolean.TRUE : Boolean.FALSE; + this.forwardingDestinations = bounded.names; + } + + public Boolean getProviderPresent() { + return providerPresent; + } + + public Boolean getListenerInitialized() { + return listenerInitialized; + } + + public Boolean getForwardingKnown() { + return forwardingKnown; + } + + public List getForwardingDestinations() { + return forwardingDestinations; + } + + private static BoundedNames boundedNames(Iterable values) { + if (values == null) { + return new BoundedNames(Collections.emptyList(), true); + } + Set unique = new LinkedHashSet(); + boolean complete = true; + for (String value : values) { + if (value == null) { + complete = false; + continue; + } + String name = value.trim(); + if (name.isEmpty() || name.length() > MAX_NAME_LENGTH) { + complete = false; + continue; + } + if (!name.equals(value) || name.codePoints().anyMatch(Character::isISOControl)) { + complete = false; + continue; + } + unique.add(name); + if (unique.size() > MAX_DESTINATIONS) { + complete = false; + unique.remove(name); + } + } + return new BoundedNames(Collections.unmodifiableList(new ArrayList(unique)), complete); + } + + private static final class BoundedNames { + private final List names; + private final boolean complete; + + private BoundedNames(List names, boolean complete) { + this.names = names; + this.complete = complete; + } + } +} diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java index f1b5889..7276d13 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java @@ -31,9 +31,11 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import com.vexsoftware.votifier.ForwardServer; -import com.vexsoftware.votifier.crypto.RSAIO; -import com.vexsoftware.votifier.crypto.RSAKeygen; -import com.vexsoftware.votifier.crypto.TokenUtil; +import com.vexsoftware.votifier.crypto.RSAIO; +import com.vexsoftware.votifier.crypto.RSAKeygen; +import com.vexsoftware.votifier.crypto.TokenUtil; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnostics; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnosticsSnapshot; import com.vexsoftware.votifier.model.Vote; import com.vexsoftware.votifier.net.ThrottleConfig; import com.vexsoftware.votifier.net.VoteReceiver; @@ -44,7 +46,12 @@ @Plugin(id = "votifierplus", name = "VotifierPlus", version = "1.0", url = "https://www.spigotmc.org/resources/votifierplus.74040", description = "Votifier Velocity Version", authors = { "BenCodez" }) -public class VotifierPlusVelocity { +public class VotifierPlusVelocity { + + /** Optional read-only management-plane snapshot; never participates in vote processing. */ + public VotifierDiagnosticsSnapshot getNetworkHealthSnapshot() { + return VotifierDiagnostics.snapshot(voteReceiver); + } @Getter private VoteReceiver voteReceiver; @Getter diff --git a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java new file mode 100644 index 0000000..d3d4f8f --- /dev/null +++ b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java @@ -0,0 +1,124 @@ +package com.bencodez.votifierplus.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.Key; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.vexsoftware.votifier.ForwardServer; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnostics; +import com.vexsoftware.votifier.diagnostic.VotifierDiagnosticsSnapshot; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.net.ThrottleConfig; +import com.vexsoftware.votifier.net.VoteReceiver; + +class VotifierDiagnosticsTest { + @Test + void unavailableReceiverDoesNotBecomeHealthy() { + VotifierDiagnosticsSnapshot snapshot = VotifierDiagnostics.snapshot(null); + + assertEquals(Boolean.TRUE, snapshot.getProviderPresent()); + assertNull(snapshot.getListenerInitialized()); + assertNull(snapshot.getForwardingKnown()); + assertTrue(snapshot.getForwardingDestinations().isEmpty()); + } + + @Test + void snapshotContainsOnlyEnabledBoundedDestinationNames() throws Exception { + TestReceiver receiver = new TestReceiver(); + VotifierDiagnosticsSnapshot snapshot = VotifierDiagnostics.snapshot(receiver); + + assertEquals(Boolean.TRUE, snapshot.getProviderPresent()); + assertEquals(Boolean.TRUE, snapshot.getListenerInitialized()); + assertEquals(Boolean.TRUE, snapshot.getForwardingKnown()); + assertEquals(Collections.singletonList("backend"), snapshot.getForwardingDestinations()); + assertFalse(snapshot.getForwardingDestinations().toString().contains("secret-token")); + receiver.getServer().close(); + assertEquals(Boolean.FALSE, VotifierDiagnostics.snapshot(receiver).getListenerInitialized()); + } + + @Test + void snapshotBoundsAndDeduplicatesNames() { + String longName = "x".repeat(81); + VotifierDiagnosticsSnapshot snapshot = new VotifierDiagnosticsSnapshot(Boolean.TRUE, Boolean.FALSE, + Boolean.TRUE, Arrays.asList("backend", "backend", "", longName, "other")); + + assertEquals(Arrays.asList("backend", "other"), snapshot.getForwardingDestinations()); + assertThrowsUnsupported(snapshot.getForwardingDestinations()); + } + + @Test + void excessEnabledDestinationsMakeForwardingEvidenceUnknown() throws Exception { + TestReceiver receiver = new TestReceiver(101); + receiver.getServer().close(); + VotifierDiagnosticsSnapshot snapshot = VotifierDiagnostics.snapshot(receiver); + + assertEquals(Boolean.FALSE, snapshot.getListenerInitialized()); + assertEquals(Boolean.FALSE, snapshot.getForwardingKnown()); + assertEquals(100, snapshot.getForwardingDestinations().size()); + } + + @Test + void platformEntryPointsExposeTheOptionalAccessor() throws Exception { + assertTrue(Class.forName("com.vexsoftware.votifier.VotifierPlus") + .getMethod("getNetworkHealthSnapshot") != null); + assertTrue(Class.forName("com.vexsoftware.votifier.bungee.VotifierPlusBungee") + .getMethod("getNetworkHealthSnapshot") != null); + assertTrue(Class.forName("com.vexsoftware.votifier.velocity.VotifierPlusVelocity") + .getMethod("getNetworkHealthSnapshot") != null); + } + + private static void assertThrowsUnsupported(List names) { + try { + names.add("unexpected"); + throw new AssertionError("snapshot list must be immutable"); + } catch (UnsupportedOperationException expected) { + // expected + } + } + + private static final class TestReceiver extends VoteReceiver { + private final int destinationCount; + + TestReceiver() throws Exception { + this(2); + } + + TestReceiver(int destinationCount) throws Exception { + super("127.0.0.1", 0); + this.destinationCount = destinationCount; + } + + @Override public boolean isUseTokens() { return false; } + @Override public ThrottleConfig getThrottleConfig() { return null; } + @Override public void logWarning(String warn) { } + @Override public void logSevere(String msg) { } + @Override public void log(String msg) { } + @Override public void debug(String msg) { } + @Override public void debug(Exception e) { } + @Override public String getVersion() { return "test"; } + @Override public Set getServers() { + if (destinationCount == 2) return Set.of("backend", "disabled"); + java.util.LinkedHashSet names = new java.util.LinkedHashSet(); + for (int i = 0; i < destinationCount; i++) names.add("backend-" + i); + return names; + } + @Override public KeyPair getKeyPair() { return null; } + @Override public Map getTokens() { return Collections.emptyMap(); } + @Override public ForwardServer getServerData(String name) { + return new ForwardServer(!"disabled".equals(name), "private-host", 8192, "secret-key", + null); + } + @Override public void callEvent(Vote vote) { } + } +} From 9c7919c56b26f38c3078fc34707690a8dde912cd Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:04:56 -0600 Subject: [PATCH 2/4] Preserve forwarding entry identity in diagnostic completeness --- .../votifier/diagnostic/VotifierDiagnostics.java | 6 +++--- .../votifierplus/tests/VotifierDiagnosticsTest.java | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java index ad9a71f..d1cd273 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java @@ -30,14 +30,14 @@ public static VotifierDiagnosticsSnapshot snapshot(VoteReceiver receiver) { } ForwardServer server = receiver.getServerData(name); if (server != null && server.isEnabled()) { - String trimmed = name.trim(); - if (trimmed.length() > VotifierDiagnosticsSnapshot.MAX_NAME_LENGTH + if (name.length() > VotifierDiagnosticsSnapshot.MAX_NAME_LENGTH || destinations.size() >= VotifierDiagnosticsSnapshot.MAX_DESTINATIONS) { // Do not claim complete evidence after the bounded snapshot omitted a risk. forwardingKnown = false; continue; } - destinations.add(trimmed); + // Preserve configured identity; snapshot validation downgrades noncanonical names. + destinations.add(name); } } } catch (RuntimeException unavailable) { diff --git a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java index d3d4f8f..f27d05f 100644 --- a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java +++ b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java @@ -68,6 +68,18 @@ void excessEnabledDestinationsMakeForwardingEvidenceUnknown() throws Exception { assertEquals(100, snapshot.getForwardingDestinations().size()); } + @Test + void distinctWhitespacePaddedTargetsDoNotBecomeCompleteNormalizedEvidence() throws Exception { + TestReceiver receiver = new TestReceiver(-1); + try { + VotifierDiagnosticsSnapshot snapshot = VotifierDiagnostics.snapshot(receiver); + assertEquals(Boolean.FALSE, snapshot.getForwardingKnown()); + assertEquals(List.of("backend"), snapshot.getForwardingDestinations()); + } finally { + receiver.getServer().close(); + } + } + @Test void platformEntryPointsExposeTheOptionalAccessor() throws Exception { assertTrue(Class.forName("com.vexsoftware.votifier.VotifierPlus") @@ -108,6 +120,7 @@ private static final class TestReceiver extends VoteReceiver { @Override public void debug(Exception e) { } @Override public String getVersion() { return "test"; } @Override public Set getServers() { + if (destinationCount == -1) return Set.of("backend", " backend "); if (destinationCount == 2) return Set.of("backend", "disabled"); java.util.LinkedHashSet names = new java.util.LinkedHashSet(); for (int i = 0; i < destinationCount; i++) names.add("backend-" + i); From 08627560a1baf344209c8f39c0b04f3d78c14e30 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:11:59 -0600 Subject: [PATCH 3/4] Safely publish listener replacements to diagnostic callers --- .../java/com/vexsoftware/votifier/VotifierPlus.java | 2 +- .../votifier/bungee/VotifierPlusBungee.java | 2 +- .../votifier/velocity/VotifierPlusVelocity.java | 2 +- .../votifierplus/tests/VotifierDiagnosticsTest.java | 10 ++++++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java index 1d4923d..fff2497 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java @@ -90,7 +90,7 @@ public VotifierDiagnosticsSnapshot getNetworkHealthSnapshot() { private Updater updater; /** The vote receiver. */ - private VoteReceiver voteReceiver; + private volatile VoteReceiver voteReceiver; /** The RSA key pair. */ @Setter diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java index 791ae15..238d650 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java @@ -40,7 +40,7 @@ public VotifierDiagnosticsSnapshot getNetworkHealthSnapshot() { } private VotifierPlusBungee instance; @Getter - private VoteReceiver voteReceiver; + private volatile VoteReceiver voteReceiver; @Getter private Config config; @Getter diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java index 7276d13..e4db590 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java @@ -53,7 +53,7 @@ public VotifierDiagnosticsSnapshot getNetworkHealthSnapshot() { return VotifierDiagnostics.snapshot(voteReceiver); } @Getter - private VoteReceiver voteReceiver; + private volatile VoteReceiver voteReceiver; @Getter private Config config; @Getter diff --git a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java index f27d05f..abb9b15 100644 --- a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java +++ b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java @@ -80,6 +80,16 @@ void distinctWhitespacePaddedTargetsDoNotBecomeCompleteNormalizedEvidence() thro } } + @Test + void receiverReplacementsAreSafelyPublishedOnEveryPlatform() throws Exception { + for (String type : List.of("com.vexsoftware.votifier.VotifierPlus", + "com.vexsoftware.votifier.bungee.VotifierPlusBungee", + "com.vexsoftware.votifier.velocity.VotifierPlusVelocity")) { + assertTrue(java.lang.reflect.Modifier.isVolatile(Class.forName(type) + .getDeclaredField("voteReceiver").getModifiers()), type); + } + } + @Test void platformEntryPointsExposeTheOptionalAccessor() throws Exception { assertTrue(Class.forName("com.vexsoftware.votifier.VotifierPlus") From 93b14a613f8d49fb014109d6d95ccba35d5bcf57 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:24:45 -0600 Subject: [PATCH 4/4] Mark unreadable forwarding entries as unknown diagnostic evidence --- .../votifier/diagnostic/VotifierDiagnostics.java | 5 ++++- .../tests/VotifierDiagnosticsTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java index d1cd273..e7cceaf 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/diagnostic/VotifierDiagnostics.java @@ -29,7 +29,10 @@ public static VotifierDiagnosticsSnapshot snapshot(VoteReceiver receiver) { continue; } ForwardServer server = receiver.getServerData(name); - if (server != null && server.isEnabled()) { + if (server == null) { + // A configured entry that cannot be classified is incomplete evidence. + forwardingKnown = false; + } else if (server.isEnabled()) { if (name.length() > VotifierDiagnosticsSnapshot.MAX_NAME_LENGTH || destinations.size() >= VotifierDiagnosticsSnapshot.MAX_DESTINATIONS) { // Do not claim complete evidence after the bounded snapshot omitted a risk. diff --git a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java index abb9b15..c61122a 100644 --- a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java +++ b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VotifierDiagnosticsTest.java @@ -80,6 +80,18 @@ void distinctWhitespacePaddedTargetsDoNotBecomeCompleteNormalizedEvidence() thro } } + @Test + void unclassifiableConfiguredTargetDoesNotBecomeDisabledForwarding() throws Exception { + TestReceiver receiver = new TestReceiver(-2); + try { + VotifierDiagnosticsSnapshot snapshot = VotifierDiagnostics.snapshot(receiver); + assertEquals(Boolean.FALSE, snapshot.getForwardingKnown()); + assertEquals(List.of(), snapshot.getForwardingDestinations()); + } finally { + receiver.getServer().close(); + } + } + @Test void receiverReplacementsAreSafelyPublishedOnEveryPlatform() throws Exception { for (String type : List.of("com.vexsoftware.votifier.VotifierPlus", @@ -131,6 +143,7 @@ private static final class TestReceiver extends VoteReceiver { @Override public String getVersion() { return "test"; } @Override public Set getServers() { if (destinationCount == -1) return Set.of("backend", " backend "); + if (destinationCount == -2) return Set.of("missing"); if (destinationCount == 2) return Set.of("backend", "disabled"); java.util.LinkedHashSet names = new java.util.LinkedHashSet(); for (int i = 0; i < destinationCount; i++) names.add("backend-" + i); @@ -139,6 +152,7 @@ private static final class TestReceiver extends VoteReceiver { @Override public KeyPair getKeyPair() { return null; } @Override public Map getTokens() { return Collections.emptyMap(); } @Override public ForwardServer getServerData(String name) { + if ("missing".equals(name)) return null; return new ForwardServer(!"disabled".equals(name), "private-host", 8192, "secret-key", null); }