From 115bd0607aafdfb1349c1d6f7e35c8b006e30480 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 28 Jul 2026 15:55:59 -0600 Subject: [PATCH 1/3] Fix empty connection timeout warnings --- .../votifier/net/VoteConnectionHandler.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/VotifierPlus/src/main/java/com/vexsoftware/votifier/net/VoteConnectionHandler.java b/VotifierPlus/src/main/java/com/vexsoftware/votifier/net/VoteConnectionHandler.java index 7632d7e..118cb41 100644 --- a/VotifierPlus/src/main/java/com/vexsoftware/votifier/net/VoteConnectionHandler.java +++ b/VotifierPlus/src/main/java/com/vexsoftware/votifier/net/VoteConnectionHandler.java @@ -63,7 +63,9 @@ public Vote handle(Socket socket) { String challenge = receiver.getChallenge(); sendHandshakeIfNeeded(in, writer, challenge); - waitForPayload(in, address); + if (!waitForPayload(in, accepted, address)) { + return null; + } String realIp = null; ProxyHeaderProcessor.ProxyHeaderResult proxyResult = proxyHeaderProcessor.process(in, writer, receiver); @@ -178,21 +180,21 @@ private void sendHandshakeIfNeeded(PushbackInputStream in, BufferedWriter writer receiver.debug("Sent handshake: " + message); } - private void waitForPayload(PushbackInputStream in, String address) throws Exception { - long waitStart = System.currentTimeMillis(); + private boolean waitForPayload(PushbackInputStream in, Socket socket, String address) throws Exception { + int previousTimeout = socket.getSoTimeout(); - while (in.available() == 0 && System.currentTimeMillis() - waitStart < 2000) { - try { - Thread.sleep(50); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - break; + try { + socket.setSoTimeout(2000); + int firstByte = in.read(); + if (firstByte == -1) { + receiver.debug("Connection closed without payload from " + address); + return false; } - } - if (in.available() == 0) { - receiver.debug("No vote payload received after handshake; closing connection from " + address); - throw new SocketTimeoutException("No vote payload received"); + in.unread(firstByte); + return true; + } finally { + socket.setSoTimeout(previousTimeout); } } @@ -209,4 +211,4 @@ private void sendOkResponse(BufferedWriter writer) { "Failed to send OK response, but will continue to process vote: " + ex.getLocalizedMessage()); } } -} \ No newline at end of file +} From 61edbbc20410f2984101b676d712cba245a8dc2f Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 28 Jul 2026 15:56:31 -0600 Subject: [PATCH 2/3] Add empty connection handling regression tests --- .../tests/EmptyConnectionHandlingTest.java | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java diff --git a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java new file mode 100644 index 0000000..4d6d637 --- /dev/null +++ b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java @@ -0,0 +1,224 @@ +package com.bencodez.votifierplus.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.crypto.Cipher; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vexsoftware.votifier.ForwardServer; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.net.ThrottleConfig; +import com.vexsoftware.votifier.net.VoteConnectionHandler; +import com.vexsoftware.votifier.net.VoteReceiver; +import com.vexsoftware.votifier.net.VoteThrottleService; + +/** + * Regression tests for empty and delayed companion connections. + */ +public class EmptyConnectionHandlingTest { + + private static KeyPair testKeyPair; + + private CapturingVoteReceiver receiver; + private ExecutorService executor; + + @BeforeAll + public static void setupClass() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + testKeyPair = keyPairGenerator.generateKeyPair(); + } + + @BeforeEach + public void setup() throws Exception { + receiver = new CapturingVoteReceiver("127.0.0.1", 0); + executor = Executors.newCachedThreadPool(); + } + + @AfterEach + public void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + if (receiver != null) { + receiver.shutdown(); + } + } + + @Test + public void testCleanCloseWithoutPayloadIsDebugOnly() throws Exception { + VoteConnectionHandler handler = new VoteConnectionHandler(receiver, new VoteThrottleService(null)); + + try (ServerSocket serverSocket = new ServerSocket(0); + Socket client = new Socket("127.0.0.1", serverSocket.getLocalPort()); + Socket accepted = serverSocket.accept()) { + Future future = executor.submit(() -> handler.handle(accepted)); + BufferedReader reader = new BufferedReader( + new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + + assertEquals("VOTIFIER 1", reader.readLine()); + client.shutdownOutput(); + + assertNull(future.get(1, TimeUnit.SECONDS)); + assertTrue(receiver.getWarnings().isEmpty()); + assertTrue(receiver.getDebugMessages().stream() + .anyMatch(message -> message.contains("Connection closed without payload"))); + } + } + + @Test + public void testSilentOpenConnectionStillWarnsAfterTimeout() throws Exception { + VoteConnectionHandler handler = new VoteConnectionHandler(receiver, new VoteThrottleService(null)); + + try (ServerSocket serverSocket = new ServerSocket(0); + Socket client = new Socket("127.0.0.1", serverSocket.getLocalPort()); + Socket accepted = serverSocket.accept()) { + Future future = executor.submit(() -> handler.handle(accepted)); + BufferedReader reader = new BufferedReader( + new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + + assertEquals("VOTIFIER 1", reader.readLine()); + assertNull(future.get(4, TimeUnit.SECONDS)); + assertTrue(receiver.getWarnings().stream() + .anyMatch(message -> message.contains("Connection timeout while waiting for vote payload"))); + } + } + + @Test + public void testDelayedPayloadPreservesFirstByteAndParsesVote() throws Exception { + VoteConnectionHandler handler = new VoteConnectionHandler(receiver, new VoteThrottleService(null)); + + try (ServerSocket serverSocket = new ServerSocket(0); + Socket client = new Socket("127.0.0.1", serverSocket.getLocalPort()); + Socket accepted = serverSocket.accept()) { + Future future = executor.submit(() -> handler.handle(accepted)); + BufferedReader reader = new BufferedReader( + new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + OutputStream output = client.getOutputStream(); + + assertEquals("VOTIFIER 1", reader.readLine()); + Thread.sleep(150); + + String voteMessage = "VOTE\nvotifier.bencodez.com\ndelayedUser\n127.0.0.1\nNormalTimestamp\n"; + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, testKeyPair.getPublic()); + output.write(cipher.doFinal(voteMessage.getBytes(StandardCharsets.UTF_8))); + output.flush(); + + Vote vote = future.get(2, TimeUnit.SECONDS); + assertNotNull(vote); + assertEquals("delayedUser", vote.getUsername()); + } + } + + private static class CapturingVoteReceiver extends VoteReceiver { + + private final List warnings = new CopyOnWriteArrayList<>(); + private final List debugMessages = new CopyOnWriteArrayList<>(); + + CapturingVoteReceiver(String host, int port) throws Exception { + super(host, port); + } + + List getWarnings() { + return warnings; + } + + List getDebugMessages() { + return debugMessages; + } + + @Override + public boolean isUseTokens() { + return false; + } + + @Override + public void logWarning(String warning) { + warnings.add(warning); + } + + @Override + public void logSevere(String message) { + } + + @Override + public void log(String message) { + } + + @Override + public void debug(String message) { + debugMessages.add(message); + } + + @Override + public void debug(Exception exception) { + debugMessages.add(exception.toString()); + } + + @Override + public String getVersion() { + return "Test"; + } + + @Override + public Set getServers() { + return Collections.emptySet(); + } + + @Override + public KeyPair getKeyPair() { + return testKeyPair; + } + + @Override + public Map getTokens() { + return Collections.emptyMap(); + } + + @Override + public ForwardServer getServerData(String server) { + return null; + } + + @Override + public void callEvent(Vote vote) { + } + + @Override + public String getChallenge() { + return "testChallenge"; + } + + @Override + public ThrottleConfig getThrottleConfig() { + return null; + } + } +} From e4adacc181989b7125bb6053df4715c44901952f Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 28 Jul 2026 15:59:50 -0600 Subject: [PATCH 3/3] Fix test receiver initialization logging --- .../tests/EmptyConnectionHandlingTest.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java index 4d6d637..3e0e2f0 100644 --- a/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java +++ b/VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/EmptyConnectionHandlingTest.java @@ -161,7 +161,9 @@ public boolean isUseTokens() { @Override public void logWarning(String warning) { - warnings.add(warning); + if (warnings != null) { + warnings.add(warning); + } } @Override @@ -174,12 +176,16 @@ public void log(String message) { @Override public void debug(String message) { - debugMessages.add(message); + if (debugMessages != null) { + debugMessages.add(message); + } } @Override public void debug(Exception exception) { - debugMessages.add(exception.toString()); + if (debugMessages != null) { + debugMessages.add(exception.toString()); + } } @Override