Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,22 @@ 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
// premium names must not reach the proxy, or it keeps verifying those players.
List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading