diff --git a/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyBridge.java b/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyBridge.java index 8945d9059..8aed2cb06 100644 --- a/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyBridge.java +++ b/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyBridge.java @@ -290,6 +290,7 @@ public void onPluginMessage(PluginMessageEvent event) { } premiumUsernames = newPremiumSet; logger.info("Premium list received from backend: " + premiumUsernames.size() + " premium player(s)"); + savePremiumNamesAsync(); } else if (PREMIUM_LIST_CHUNK_MESSAGE.equals(parsedMessage.typeId())) { String[] parts = parsedMessage.playerName().split(":", 3); if (parts.length < 3) { diff --git a/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeProxyBridgeTest.java b/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeProxyBridgeTest.java index 51732e092..1221edd74 100644 --- a/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeProxyBridgeTest.java +++ b/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeProxyBridgeTest.java @@ -14,6 +14,7 @@ import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.event.ServerSwitchEvent; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -26,6 +27,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -487,6 +490,44 @@ private static byte[] createChunkPayload(int seq, boolean last, String csv) { return output.toByteArray(); } + @Test + void shouldPersistNonChunkedPremiumListToCache(@TempDir Path tempDir) throws Exception { + given(pluginMessageEvent.isCancelled()).willReturn(false); + given(pluginMessageEvent.getTag()).willReturn(BungeeProxyBridge.AUTHME_CHANNEL); + given(pluginMessageEvent.getSender()).willReturn(sourceServer); + given(pluginMessageEvent.getData()).willReturn(createListPayload("Alice")); + + BungeeProxyBridge bridge = new BungeeProxyBridge(proxyServer, logger, createConfiguration(), new BungeeAuthenticationStore(), tempDir); + bridge.onPluginMessage(pluginMessageEvent); + + Path cacheFile = tempDir.resolve("premium_names.cache"); + long deadline = System.currentTimeMillis() + 5000; + while (System.currentTimeMillis() < deadline) { + if (Files.exists(cacheFile) && Files.readString(cacheFile).contains("alice")) { + break; + } + Thread.sleep(50); + } + assertTrue(Files.exists(cacheFile), "premium cache file should be written"); + bridge.shutdown(); + + BungeeProxyBridge restarted = new BungeeProxyBridge(proxyServer, logger, createConfiguration(), new BungeeAuthenticationStore(), tempDir); + given(playerHandshakeEvent.getConnection()).willReturn(pendingConnection); + given(pendingConnection.getName()).willReturn("Alice"); + given(pendingConnection.isOnlineMode()).willReturn(false); + restarted.onPlayerHandshake(playerHandshakeEvent); + restarted.shutdown(); + + verify(pendingConnection).setOnlineMode(true); + } + + private static byte[] createListPayload(String csv) { + ByteArrayDataOutput output = ByteStreams.newDataOutput(); + output.writeUTF("premium.list"); + output.writeUTF(csv); + return output.toByteArray(); + } + private static BungeeProxyConfiguration createConfiguration() { return new BungeeProxyConfiguration( Set.of("lobby"), false, true, Set.of("/login", "/register", "/l", "/reg", "/email", "/captcha", "/2fa", "/totp", "/log"), diff --git a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java index 56f0d93a8..49e0269c3 100644 --- a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java +++ b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java @@ -114,6 +114,7 @@ public void onPluginMessageReceived(String channel, Player player, byte[] data) if (type.get() == MessageType.PROXY_STARTED) { logger.info("Proxy plugin '" + argument + "' has started and registered the authme:main channel"); final String proxyName = argument; + final Player triggeringPlayer = player; bukkitService.runTaskAsynchronously(() -> { // Always send the list, even when it is empty: an empty list is authoritative and // lets the proxy replace a stale premium cache. With premium disabled, stored @@ -121,11 +122,14 @@ public void onPluginMessageReceived(String channel, Player player, byte[] data) List premiumNames = premiumEnabled ? dataSource.getPremiumUsernames() : List.of(); bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { // Re-fetch a carrier at send-time: the original player may have gone offline - // during the async DB query. + // during the async DB query. Fall back to the triggering player so the list + // still goes out when no fully-joined player exists yet (first join during + // the login phase) or the single online player disconnected mid-query. Player freshCarrier = bukkitService.getOnlinePlayers().stream() .findFirst().orElse(null); - if (freshCarrier != null) { - bungeeSender.sendPremiumList(freshCarrier, premiumNames); + Player carrier = freshCarrier != null ? freshCarrier : triggeringPlayer; + if (carrier != null) { + bungeeSender.sendPremiumList(carrier, premiumNames); logger.info("Sent premium list (" + premiumNames.size() + " player(s)) to proxy '" + proxyName + "'"); } else { logger.warning("Cannot send premium list to proxy '" + proxyName diff --git a/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java b/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java index faec09210..b2a6993fe 100644 --- a/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java @@ -337,6 +337,28 @@ void shouldSendEmptyPremiumListOnProxyStartedWhenNoPremiumUsersAreStored() { verify(bungeeSender).sendPremiumList(carrier, List.of()); } + @Test + void shouldSendPremiumListViaTriggeringPlayerWhenNoOnlinePlayers() { + // given + given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true); + given(dataSource.getPremiumUsernames()).willReturn(List.of("Alice")); + setBukkitServiceToRunTaskAsynchronously(bukkitService); + setBukkitServiceToScheduleSyncTaskFromOptionallyAsyncTask(bukkitService); + given(bukkitService.getOnlinePlayers()).willReturn(List.of()); + + Player triggeringPlayer = mock(Player.class); + + BungeeReceiver receiver = + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, + proxyLoginRequestValidator, settings); + + // when + receiver.onPluginMessageReceived("authme:main", triggeringPlayer, buildProxyStartedPayload("velocity")); + + // then + verify(bungeeSender).sendPremiumList(triggeringPlayer, List.of("Alice")); + } + private static byte[] buildProxyStartedPayload(String proxyName) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF(MessageType.PROXY_STARTED.getId()); diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java index f7280145e..7abe25702 100644 --- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java +++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyBridge.java @@ -296,6 +296,7 @@ void onPluginMessage(PluginMessageEvent event) { } premiumUsernames = newPremiumSet; logger.info("Premium list received from backend: {} premium player(s)", premiumUsernames.size()); + savePremiumNamesAsync(); } else if (PREMIUM_LIST_CHUNK_MESSAGE.equals(parsedMessage.typeId())) { String[] parts = parsedMessage.playerName().split(":", 3); if (parts.length < 3) { diff --git a/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java b/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java index 8eb49b5cc..c9615619c 100644 --- a/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java +++ b/authme-velocity/src/test/java/fr/xephi/authme/velocity/VelocityProxyBridgeTest.java @@ -22,6 +22,7 @@ import com.velocitypowered.api.proxy.server.ServerInfo; import net.kyori.adventure.text.Component; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -33,6 +34,8 @@ import java.util.Optional; import java.util.Set; +import java.nio.file.Files; +import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -668,6 +671,45 @@ private static byte[] createChunkPayload(int seq, boolean last, String csv) { return output.toByteArray(); } + @Test + void shouldPersistNonChunkedPremiumListToCache(@TempDir Path tempDir) throws Exception { + given(pluginMessageEvent.getResult()).willReturn(PluginMessageEvent.ForwardResult.forward()); + given(pluginMessageEvent.getIdentifier()).willReturn(VelocityProxyBridge.AUTHME_CHANNEL); + given(pluginMessageEvent.getSource()).willReturn(sourceConnection); + given(pluginMessageEvent.getData()).willReturn(createListPayload("Alice")); + given(sourceConnection.getServer()).willReturn(authServer); + given(authServer.getServerInfo()).willReturn(authServerInfo); + given(authServerInfo.getName()).willReturn("lobby"); + + VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), tempDir); + bridge.onPluginMessage(pluginMessageEvent); + + Path cacheFile = tempDir.resolve("premium_names.cache"); + long deadline = System.currentTimeMillis() + 5000; + while (System.currentTimeMillis() < deadline) { + if (Files.exists(cacheFile) && Files.readString(cacheFile).contains("alice")) { + break; + } + Thread.sleep(50); + } + assertTrue(Files.exists(cacheFile), "premium cache file should be written"); + bridge.shutdown(); + + VelocityProxyBridge restarted = new VelocityProxyBridge(proxyServer, logger, createConfiguration(), new VelocityAuthenticationStore(), tempDir); + PreLoginEvent event = new PreLoginEvent(mock(InboundConnection.class), "Alice", null); + restarted.onPreLogin(event); + restarted.shutdown(); + + assertEquals(PreLoginEvent.PreLoginComponentResult.forceOnlineMode().toString(), event.getResult().toString()); + } + + private static byte[] createListPayload(String csv) { + ByteArrayDataOutput output = ByteStreams.newDataOutput(); + output.writeUTF("premium.list"); + output.writeUTF(csv); + return output.toByteArray(); + } + private static VelocityProxyConfiguration createConfiguration() { return new VelocityProxyConfiguration(Set.of("lobby"), false, true, "Authentication required.", true, false, "", true,