Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
567531e
Add secure single-port HTTP proxy transport
BenCodez Aug 31, 2026
1fd3be9
Validate HTTP client certificates at TLS boundary
BenCodez Aug 31, 2026
d27636b
Harden HTTP transport lifecycle
BenCodez Aug 31, 2026
1d30c94
Address HTTP transport review findings
BenCodez Aug 31, 2026
e9bbe03
Harden HTTP shutdown and protocol parsing
BenCodez Aug 31, 2026
38a37dd
fix(http): retain votes rejected by transport queue
BenCodez Aug 31, 2026
5722685
fix(http): persist outbound deliveries until ack
BenCodez Aug 31, 2026
aa0f8d8
fix(http): persist poll-created backend queues
BenCodez Aug 31, 2026
63e8f9d
fix(http): validate enrollment codes in Control
BenCodez Aug 31, 2026
5b666dc
fix(http): serialize callbacks and renew transport CA
BenCodez Sep 1, 2026
dbf416f
fix(http): verify backend presence against proxy route
BenCodez Sep 1, 2026
ac6a947
fix(http): preserve proxy callback order
BenCodez Sep 1, 2026
f104e61
fix(http): preserve callback order and crash dedup
BenCodez Sep 1, 2026
13fd2b7
fix(http): normalize corrupt fence errors
BenCodez Sep 1, 2026
71f5ca4
fix(http): revoke pending enrollment codes
BenCodez Sep 1, 2026
6216a04
fix(http): distinguish incomplete inbound deliveries
BenCodez Sep 1, 2026
2a9fcc6
test(http): import filesystem helper
BenCodez Sep 1, 2026
9eae8b5
fix(http): bound responses and persist queue roots
BenCodez Sep 1, 2026
b13ce28
test(http): await resumed delivery acknowledgement
BenCodez Sep 1, 2026
0a938cf
fix(http): persist transport directory entries
BenCodez Sep 1, 2026
5949e11
fix(http): coordinate transport lifecycle handoffs
BenCodez Sep 1, 2026
dd1ef83
fix(ci): restore complete proxy source
BenCodez Sep 1, 2026
0e9cf5a
fix(test): disambiguate transport validation assertion
BenCodez Sep 1, 2026
f81aa90
fix(http): validate readiness off the server thread
BenCodez Sep 1, 2026
b7503b9
fix(control): cancel stale transport publication
BenCodez Sep 1, 2026
67e9d12
fix(http): open proxy listener after runtime setup
BenCodez Sep 1, 2026
425664b
Restore HTTP transport after failed control reload
BenCodez Sep 1, 2026
c8b0116
Merge master into codex/http-transport
BenCodez Sep 1, 2026
57517d7
fix(http): restore credentials after failed reload
BenCodez Sep 1, 2026
9b9232b
fix(http): preserve independent delivery retries
BenCodez Sep 2, 2026
59b0805
fix(http): retain rejected vote-party rewards
BenCodez Sep 2, 2026
c10236a
fix(http): import vote-party collection helpers
BenCodez Sep 2, 2026
7fec797
test(http): normalize legacy code expiry precision
BenCodez Sep 2, 2026
01dea25
fix(http): bound streamed response reads
BenCodez Sep 2, 2026
f811389
fix(http): cancel stalled body reads at deadline
BenCodez Sep 2, 2026
7495519
fix(http): recover incomplete TLS provisioning
BenCodez Sep 2, 2026
e23a969
fix(http): defer replacement presence publication
BenCodez Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion VotingPlugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<velocity.version>3.4.0</velocity.version>
<bouncycastle.version>1.85</bouncycastle.version>
</properties>
<build>
<resources>
Expand Down Expand Up @@ -151,6 +152,10 @@
<shadedPattern>
${project.groupId}.votingplugin.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.bouncycastle</pattern>
<shadedPattern>${project.groupId}.votingplugin.bouncycastle</shadedPattern>
</relocation>
<relocation>
<pattern>xyz.upperlevel.spigot</pattern>
<shadedPattern>
Expand Down Expand Up @@ -254,6 +259,16 @@
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
Expand Down Expand Up @@ -692,4 +707,4 @@
</build>
</profile>
</profiles>
</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -1214,36 +1214,136 @@ public String getBackendHostedControlStatus() {
}

/** Recreates proxy transports after Control applies BungeeSettings.yml. */
public synchronized void restartBackendProxyHandler() {
public void restartBackendProxyHandler() {
restartBackendProxyHandler(System.nanoTime() + TimeUnit.SECONDS.toNanos(25));
}

/** Recreates proxy transports while preserving the caller's end-to-end validation deadline. */
public void restartBackendProxyHandler(long validationDeadlineNanos) {
BackendProxyRestart restart = prepareBackendProxyHandlerRestart();
try {
validateBackendProxyHandlerRestart(restart, validationDeadlineNanos);
completeBackendProxyHandlerRestart(restart);
} catch (RuntimeException failure) {
abortBackendProxyHandlerRestart(restart);
throw failure;
}
}

/** Prepared on the Bukkit thread, validated off-thread, then atomically published on Bukkit. */
public static final class BackendProxyRestart {
private final BackendProxyHandler previous;
private final BackendProxyHandler replacement;
private final boolean disabled;
private final boolean previousPrepared;
private boolean finished;
private boolean abandonmentRequested;
private volatile boolean published;

private BackendProxyRestart(BackendProxyHandler previous, BackendProxyHandler replacement, boolean disabled,
boolean previousPrepared) {
this.previous = previous;
this.replacement = replacement;
this.disabled = disabled;
this.previousPrepared = previousPrepared;
}
}

public static final class BackendProxyRestartPreparationException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final BackendProxyRestart restart;

private BackendProxyRestartPreparationException(BackendProxyRestart restart, RuntimeException cause) {
super(cause);
this.restart = restart;
}

public BackendProxyRestart restart() { return restart; }
}

public synchronized BackendProxyRestart prepareBackendProxyHandlerRestart() {
BackendProxyHandler previous = backendProxyHandler;
if (!bungeeSettings.isUseBungeecoord()) {
backendProxyHandler = null;
if (previous != null) previous.close();
BackendControlAutoEnrollment enrollment = backendControlAutoEnrollment;
backendControlAutoEnrollment = null;
if (enrollment != null) enrollment.close();
return;
return new BackendProxyRestart(previous, null, true, false);
}
BungeeMethod replacementMethod = BungeeMethod.getByName(bungeeSettings.getBungeeMethod());
if (previous != null) previous.prepareForReplacement(replacementMethod);
boolean previousPrepared = previous != null && previous.prepareForReplacement(replacementMethod);
BackendProxyHandler replacement = new BackendProxyHandler(this, backendProcessedVoteCache);
try {
replacement.load();
replacement.validateTransport();
if (previous != null) previous.completeRedisHandoff(replacement);
replacement.loadForReplacement();
} catch (RuntimeException failure) {
replacement.close();
if (previousPrepared) {
previous.restoreAfterFailedReplacement();
BackendProxyRestart failed = new BackendProxyRestart(previous, null, false, true);
failed.finished = true;
throw new BackendProxyRestartPreparationException(failed, failure);
}
throw failure;
}
backendProxyHandler = replacement;
if (previous != null) previous.close();
return new BackendProxyRestart(previous, replacement, false, previousPrepared);
}

public void validateBackendProxyHandlerRestart(BackendProxyRestart restart, long validationDeadlineNanos) {
if (restart == null) throw new IllegalArgumentException("Backend proxy restart is required");
if (restart.replacement != null) restart.replacement.validateTransport(validationDeadlineNanos);
}

public void completeBackendProxyHandlerRestart(BackendProxyRestart restart) {
synchronized (this) {
if (restart == null || restart.finished) throw new IllegalStateException("Backend proxy restart is no longer active");
if (restart.abandonmentRequested) {
abortBackendProxyHandlerRestart(restart);
return;
}
if (backendProxyHandler != restart.previous) throw new IllegalStateException("Backend proxy handler changed during restart");
if (restart.disabled) {
backendProxyHandler = null;
if (restart.previous != null) restart.previous.close();
BackendControlAutoEnrollment enrollment = backendControlAutoEnrollment;
backendControlAutoEnrollment = null;
if (enrollment != null) enrollment.close();
restart.finished = true;
restart.published = true;
return;
}
if (restart.previous != null) restart.previous.completeRedisHandoff(restart.replacement);
restart.replacement.activatePresenceReporting();
backendProxyHandler = restart.replacement;
if (restart.previous != null) restart.previous.close();
restart.finished = true;
restart.published = true;
}
try {
refreshBackendControlAutoEnrollment();
} catch (IOException e) {
getLogger().warning("[Control] Automatic backend enrollment was not refreshed: " + e.getMessage());
}
}

/** Returns false once publication committed and can no longer be rolled back as a failed apply. */
public synchronized boolean requestBackendProxyHandlerRestartAbandonment(BackendProxyRestart restart) {
if (restart == null) return true;
if (restart.published) return false;
restart.abandonmentRequested = true;
return true;
}

public synchronized void abortBackendProxyHandlerRestart(BackendProxyRestart restart) {
if (restart == null || restart.finished) return;
if (restart.replacement != null) restart.replacement.close();
Comment thread
BenCodez marked this conversation as resolved.
if (restart.previousPrepared && backendProxyHandler == restart.previous) {
restart.previous.restoreAfterFailedReplacement();
}
restart.finished = true;
}

public void awaitBackendProxyHandlerRollback(BackendProxyRestart restart, long deadlineNanos) {
if (restart != null && restart.previousPrepared) {
restart.previous.awaitRestoreAfterFailedReplacement(deadlineNanos);
}
}

/** Keeps one plugin-message listener for the plugin lifetime and atomically swaps its active backend handler. */
public synchronized void activateBackendPluginMessageHandler(GlobalMessageHandler target) {
backendPluginMessageTarget.set(target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class BackendProxyHandler implements Listener {
private final BackendGlobalDataSync globalDataSync;

private BackendPresenceManager presenceManager;
private boolean presenceReportingActivated;
private BackendVotePartySync votePartySync;
private BackendProxyMessageRouter messageRouter;

Expand All @@ -62,6 +63,15 @@ public BackendProxyHandler(VotingPluginMain plugin, ProcessedVoteCache processed
* Loads the configured backend/proxy communication components.
*/
public void load() {
load(true);
}

/** Loads a replacement without announcing a new presence generation before publication. */
public void loadForReplacement() {
load(false);
}

private void load(boolean activatePresenceReporting) {
plugin.debug("Loading backend proxy handler");
method = BungeeMethod.getByName(plugin.getBungeeSettings().getBungeeMethod());
plugin.getLogger().info("Using BungeeMethod: " + method.toString());
Expand All @@ -84,15 +94,24 @@ public void sendMessage(JsonEnvelope envelope) {
if (plugin.getOptions().getServer().equalsIgnoreCase("pleaseset")) {
plugin.getLogger().warning("Server name for bungee voting is not set, please set it");
}
presenceManager.start();
if (activatePresenceReporting) activatePresenceReporting();
}

/** Starts presence only after a staged handler reaches the atomic publication boundary. */
public void activatePresenceReporting() {
if (presenceManager != null && !presenceReportingActivated) {
presenceManager.start();
presenceReportingActivated = true;
}
}

/**
* Closes backend/proxy components and persists cached proxy state.
*/
public void close() {
if (presenceManager != null) {
if (presenceManager != null && presenceReportingActivated) {
presenceManager.stop();
presenceReportingActivated = false;
}
transportManager.close();
if (votePartySync != null) {
Expand All @@ -102,18 +121,34 @@ public void close() {
}

/** Releases a same-method subscriber/listener before its replacement starts. */
public void prepareForReplacement(BungeeMethod replacementMethod) {
public boolean prepareForReplacement(BungeeMethod replacementMethod) {
if (method == replacementMethod && method != BungeeMethod.PLUGINMESSAGING && method != BungeeMethod.REDIS) {
transportManager.prepareForReplacement();
return method == BungeeMethod.HTTP;
}
return false;
}

/** Restores a prepared HTTP transport when its replacement fails validation. */
public void restoreAfterFailedReplacement() {
transportManager.restorePreparedTransport();
}

public void awaitRestoreAfterFailedReplacement(long deadlineNanos) {
transportManager.awaitPreparedTransportRestoration(deadlineNanos);
}

/** Fails a configuration apply when its selected transport did not initialize. */
public void validateTransport() {
validateTransport(System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(25));
}

/** Validates transport startup without extending the caller's existing deadline. */
public void validateTransport(long deadlineNanos) {
if (method == null || globalMessageHandler == null || presenceManager == null) {
throw new IllegalStateException("Backend proxy handler initialization failed");
}
transportManager.validate();
transportManager.validate(deadlineNanos);
}

/** Completes the no-loss/no-duplicate same-Redis subscriber handoff after validation. */
Expand All @@ -136,14 +171,15 @@ public void playerOffline(String playerName) {
}

public void reloadPresenceReporting() {
if (presenceManager != null) {
if (presenceManager != null && presenceReportingActivated) {
presenceManager.reload();
}
}

public void disablePresenceReporting() {
if (presenceManager != null) {
if (presenceManager != null && presenceReportingActivated) {
presenceManager.stop();
presenceReportingActivated = false;
}
}

Expand Down
Loading
Loading