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 @@ -3,6 +3,8 @@
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import fr.xephi.authme.bungee.events.AuthMeBungeeLoginEvent;
import fr.xephi.authme.bungee.events.AuthMeBungeeLogoutEvent;
import fr.xephi.authme.bungee.premium.BungeePremiumOnlineModeHandler;
import fr.xephi.authme.bungee.premium.BungeePremiumVerificationManager;
import net.md_5.bungee.api.ChatColor;
Expand Down Expand Up @@ -244,11 +246,21 @@ public void onPluginMessage(PluginMessageEvent event) {
return;
}

String playerName = normalizeName(parsedMessage.playerName());

if (LOGIN_MESSAGE.equals(parsedMessage.typeId())) {
if (configuration.isAuthServer(server.getInfo())) {
logger.info("Player " + parsedMessage.playerName() + " authenticated on auth server '"
+ server.getInfo().getName() + "'");

ProxiedPlayer player = proxyServer.getPlayer(playerName);
if (player != null) {
boolean premium = requiresPremiumVerification(parsedMessage.playerName());
proxyServer.getPluginManager().callEvent(new AuthMeBungeeLoginEvent(player, premium));
}
Comment on lines +256 to +260

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPremium() is always false for premium players (non-normalized name passed to premium lookup).
The new code computes boolean premium = requiresPremiumVerification(parsedMessage.playerName()) using the raw player name (e.g. "Alice"), but requiresPremiumVerification checks premiumUsernames / pendingPremiumUsernames, which store lowercase-normalized names (populated via normalizeName(...) at lines 287/306). So for any mixed/upper-case username, the lookup misses and AuthMeBungeeLoginEvent.isPremium() reports false even for genuinely premium players. The normalized playerName variable was just computed three lines above and should be used here (line 249) exactly as the Velocity side does (requiresPremiumVerification(normalizeName(...))).
Btw, the new test uses a non-premium player, so it doesn't catch this.


authenticationStore.markAuthenticated(parsedMessage.playerName());

sendAutoLoginIfAlreadySwitched(parsedMessage.playerName(), server.getInfo());
redirectToLoginServer(parsedMessage.playerName());
} else if (pendingAutoLogins.containsKey(parsedMessage.playerName())) {
Expand All @@ -259,6 +271,11 @@ public void onPluginMessage(PluginMessageEvent event) {
}
} else if (LOGOUT_MESSAGE.equals(parsedMessage.typeId())) {
authenticationStore.markLoggedOut(parsedMessage.playerName());
ProxiedPlayer player = proxyServer.getPlayer(playerName);
if (player != null) {
proxyServer.getPluginManager().callEvent(new AuthMeBungeeLogoutEvent(player));
}

redirectLoggedOutPlayer(parsedMessage.playerName());
} else if (PERFORM_LOGIN_ACK_MESSAGE.equals(parsedMessage.typeId())) {
logger.info("Auto-login ACK received for " + parsedMessage.playerName()
Expand Down Expand Up @@ -350,7 +367,6 @@ public void onServerSwitch(ServerSwitchEvent event) {
logger.fine("PacketEvents-verified premium player " + normalizedName
+ " joining auth server — sending perform.login immediately");
}

String serverName = currentServer.getInfo().getName();
logger.info("Sending auto-login request to server '" + serverName + "' for player " + normalizedName);
currentServer.getInfo().sendData(
Expand Down Expand Up @@ -447,6 +463,7 @@ private void sendAutoLoginIfAlreadySwitched(String normalizedName, ServerInfo au
// Still on auth server — normal flow, ServerSwitchEvent will handle it on switch
return;
}

String currentServerName = currentConn.getInfo().getName();
logger.info("Player " + normalizedName + " already on server '" + currentServerName
+ "' when login message arrived — sending auto-login immediately");
Expand Down Expand Up @@ -508,6 +525,7 @@ private void scheduleRetry(String normalizedName) {
logger.fine("Auto-login retry cancelled for " + normalizedName + " (player has no active server)");
return;
}

String serverName = server.getInfo().getName();
logger.fine("Retrying auto-login for " + normalizedName + " on server '" + serverName
+ "' (attempt " + (current + 1) + "/" + MAX_RETRIES + ")");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.xephi.authme.bungee.events;

import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Event;

public class AuthMeBungeeLoginEvent extends Event {

private final ProxiedPlayer player;

private final boolean premium;

public AuthMeBungeeLoginEvent(ProxiedPlayer player, boolean premium) {
this.player = player;
this.premium = premium;
}

/**
* Return whether this player required Premium verification for login
*
* @return if the player required premium verification
*/
public boolean isPremium() {
return premium;
}

/**
* Return the player concerned by this event.
*
* @return The player who logged in correctly in the backend
*/
public ProxiedPlayer getPlayer() {
return player;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fr.xephi.authme.bungee.events;

import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Event;

public class AuthMeBungeeLogoutEvent extends Event {

private final ProxiedPlayer player;

public AuthMeBungeeLogoutEvent(ProxiedPlayer player) {
this.player = player;
}

/**
* Return the player concerned by this event.
*
* @return The player who logged out correctly in the backend
*/
public ProxiedPlayer getPlayer() {
return player;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import fr.xephi.authme.bungee.events.AuthMeBungeeLoginEvent;
import fr.xephi.authme.bungee.events.AuthMeBungeeLogoutEvent;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.PendingConnection;
Expand All @@ -13,6 +15,7 @@
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectEvent;
import net.md_5.bungee.api.event.ServerSwitchEvent;
import net.md_5.bungee.api.plugin.PluginManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
Expand All @@ -29,6 +32,8 @@
import java.util.logging.Logger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -85,6 +90,9 @@ class BungeeProxyBridgeTest {
@Mock
private PendingConnection pendingConnection;

@Mock
private PluginManager pluginManager;

@Captor
private ArgumentCaptor<byte[]> payloadCaptor;

Expand Down Expand Up @@ -160,6 +168,7 @@ void shouldRedirectPlayerOnLogoutWhenConfigured() {
given(proxyServer.getPlayer("alice")).willReturn(player);
given(proxyServer.getServerInfo("limbo")).willReturn(nonAuthServerInfo);

stubEventsAllowed();
BungeeProxyBridge bridge = new BungeeProxyBridge(
proxyServer, logger, new BungeeProxyConfiguration(
Set.of("lobby"), false, true, Set.of("/login"), true, true,
Expand Down Expand Up @@ -204,8 +213,8 @@ void shouldCancelPendingLoginOnExplicitAck() {
given(pluginMessageEvent.getData()).willReturn(createAuthMePayload("perform.login.ack", "Alice"));
bridge.onPluginMessage(pluginMessageEvent);

// getPlayer called exactly once by sendAutoLoginIfAlreadySwitched (on login), not by any retry
verify(proxyServer, org.mockito.Mockito.times(1)).getPlayer("alice");
// getPlayer called twice: once by login event lookup, once by sendAutoLoginIfAlreadySwitched — not by any retry
verify(proxyServer, org.mockito.Mockito.times(2)).getPlayer("alice");
}

@Test
Expand Down Expand Up @@ -233,8 +242,8 @@ void shouldCancelPendingLoginOnImplicitAckFromNonAuthServer() {
given(sourceServer.getInfo()).willReturn(nonAuthServerInfo);
bridge.onPluginMessage(pluginMessageEvent);

// getPlayer called exactly once by sendAutoLoginIfAlreadySwitched (on login from auth server), not by retries
verify(proxyServer, org.mockito.Mockito.times(1)).getPlayer("alice");
// getPlayer called twice: once by login event lookup, once by sendAutoLoginIfAlreadySwitched — not by retries
verify(proxyServer, org.mockito.Mockito.times(2)).getPlayer("alice");
}

@Test
Expand Down Expand Up @@ -349,7 +358,6 @@ void shouldNotForwardPerformLoginToNonAuthServers() {
given(sourceServer.getInfo()).willReturn(authServerInfo);
given(authServerInfo.getName()).willReturn("lobby");
given(serverSwitchEvent.getPlayer()).willReturn(player);
given(player.getName()).willReturn("Alice");
given(player.getServer()).willReturn(currentServer);
given(currentServer.getInfo()).willReturn(nonAuthServerInfo);
given(nonAuthServerInfo.getName()).willReturn("survival");
Expand Down Expand Up @@ -439,6 +447,7 @@ void shouldSendAutoLoginImmediatelyWhenPlayerAlreadySwitchedBeforeLoginMessage()
given(currentServer.getInfo()).willReturn(nonAuthServerInfo);
given(nonAuthServerInfo.getName()).willReturn("survival");

stubEventsAllowed();
BungeeProxyBridge bridge = new BungeeProxyBridge(proxyServer, logger, createConfiguration(), new BungeeAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);

Expand Down Expand Up @@ -480,13 +489,59 @@ void shouldForceOnlineModeForPremiumHandshakeAfterChunkedPremiumListResync() {
verify(pendingConnection).setOnlineMode(true);
}

@Test
void shouldFireLoginEventWhenPlayerAuthenticatesOnAuthServer() {
given(pluginMessageEvent.isCancelled()).willReturn(false);
given(pluginMessageEvent.getTag()).willReturn(BungeeProxyBridge.AUTHME_CHANNEL);
given(pluginMessageEvent.getSender()).willReturn(sourceServer);
given(pluginMessageEvent.getData()).willReturn(createAuthMePayload("login", "Alice"));
given(sourceServer.getInfo()).willReturn(authServerInfo);
given(authServerInfo.getName()).willReturn("lobby");
given(proxyServer.getPlayer("alice")).willReturn(player);
given(player.getServer()).willReturn(currentServer);
given(currentServer.getInfo()).willReturn(authServerInfo);

stubEventsAllowed();

BungeeProxyBridge bridge = new BungeeProxyBridge(proxyServer, logger, createConfiguration(), new BungeeAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);

ArgumentCaptor<AuthMeBungeeLoginEvent> eventCaptor = ArgumentCaptor.forClass(AuthMeBungeeLoginEvent.class);
verify(pluginManager).callEvent(eventCaptor.capture());
assertSame(player, eventCaptor.getValue().getPlayer());
assertFalse(eventCaptor.getValue().isPremium());
}

@Test
void shouldFireLogoutEventWhenPlayerLogsOut() {
given(pluginMessageEvent.isCancelled()).willReturn(false);
given(pluginMessageEvent.getTag()).willReturn(BungeeProxyBridge.AUTHME_CHANNEL);
given(pluginMessageEvent.getSender()).willReturn(sourceServer);
given(pluginMessageEvent.getData()).willReturn(createAuthMePayload("logout", "Alice"));
given(proxyServer.getPlayer("alice")).willReturn(player);

stubEventsAllowed();

BungeeProxyBridge bridge = new BungeeProxyBridge(proxyServer, logger, createConfiguration(), new BungeeAuthenticationStore(), null);
bridge.onPluginMessage(pluginMessageEvent);

ArgumentCaptor<AuthMeBungeeLogoutEvent> eventCaptor = ArgumentCaptor.forClass(AuthMeBungeeLogoutEvent.class);
verify(pluginManager).callEvent(eventCaptor.capture());
assertSame(player, eventCaptor.getValue().getPlayer());
}

private static byte[] createChunkPayload(int seq, boolean last, String csv) {
ByteArrayDataOutput output = ByteStreams.newDataOutput();
output.writeUTF("premium.list.chunk");
output.writeUTF(seq + ":" + (last ? "1" : "0") + ":" + csv);
return output.toByteArray();
}

private void stubEventsAllowed() {
given(proxyServer.getPluginManager()).willReturn(pluginManager);
given(pluginManager.callEvent(any())).willAnswer(inv -> inv.getArgument(0));
}

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 @@ -19,6 +19,8 @@
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import fr.xephi.authme.velocity.events.AuthMeVelocityLoginEvent;
import fr.xephi.authme.velocity.events.AuthMeVelocityLogoutEvent;
import fr.xephi.authme.velocity.premium.VelocityPremiumVerificationManager;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
Expand Down Expand Up @@ -242,11 +244,20 @@ void onPluginMessage(PluginMessageEvent event) {
return;
}

String normalizedName = normalizeName(parsedMessage.playerName());
String serverName = serverConnection.getServer().getServerInfo().getName();

if (LOGIN_MESSAGE.equals(parsedMessage.typeId())) {
if (configuration.isAuthServer(serverConnection.getServer())) {
logger.info("Player {} authenticated on auth server '{}'", parsedMessage.playerName(), serverName);

proxyServer.getPlayer(normalizedName).ifPresent(player -> {
boolean premium = requiresPremiumVerification(normalizeName(parsedMessage.playerName()));
AuthMeVelocityLoginEvent loginEvent = new AuthMeVelocityLoginEvent(player, premium);

proxyServer.getEventManager().fireAndForget(loginEvent);
});

authenticationStore.markAuthenticated(parsedMessage.playerName());
sendAutoLoginIfAlreadySwitched(parsedMessage.playerName(), serverConnection.getServer());
redirectToLoginServer(parsedMessage.playerName());
Expand All @@ -262,6 +273,11 @@ void onPluginMessage(PluginMessageEvent event) {
} else if (LOGOUT_MESSAGE.equals(parsedMessage.typeId())) {
logger.info("Player {} logged out (notified by server '{}')", parsedMessage.playerName(), serverName);
authenticationStore.markLoggedOut(parsedMessage.playerName());
proxyServer.getPlayer(normalizedName).ifPresent(player -> {
AuthMeVelocityLogoutEvent loginEvent = new AuthMeVelocityLogoutEvent(player);
proxyServer.getEventManager().fireAndForget(loginEvent);
});

redirectLoggedOutPlayer(parsedMessage.playerName());
} else if (PERFORM_LOGIN_ACK_MESSAGE.equals(parsedMessage.typeId())) {
logger.info("Auto-login ACK received for {} from server '{}'",
Expand Down Expand Up @@ -567,9 +583,11 @@ private void scheduleRetry(String normalizedName) {
String serverName = serverOpt.get().getServer().getServerInfo().getName();
logger.debug("Retrying auto-login for {} on server '{}' (attempt {}/{})",
normalizedName, serverName, current + 1, MAX_RETRIES);

UUID verifiedPremiumUuid = premiumVerificationManager.getVerifiedPremiumUuid(normalizedName);
serverOpt.get().sendPluginMessage(AUTHME_CHANNEL,
createPerformLoginMessage(normalizedName, verifiedPremiumUuid));

scheduleRetry(normalizedName);
}, 1, TimeUnit.SECONDS);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.xephi.authme.velocity.events;

import com.velocitypowered.api.proxy.Player;

public class AuthMeVelocityLoginEvent {


private final Player player;
private final boolean premium;

public AuthMeVelocityLoginEvent(Player player, boolean premium) {
this.player = player;
this.premium = premium;
}

/**
* Return the player concerned by this event.
*
* @return The player who logged in correctly in the backend
*/
public Player getPlayer() {
return player;
}


/**
* Return whether this player required Premium verification for login
*
* @return if the player required premium verification
*/
public boolean isPremium() {
return premium;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fr.xephi.authme.velocity.events;

import com.velocitypowered.api.proxy.Player;

public class AuthMeVelocityLogoutEvent {

private final Player player;

public AuthMeVelocityLogoutEvent(Player player) {
this.player = player;
}

/**
* Return the player concerned by this event.
*
* @return The player who logged out correctly in the backend
*/
public Player getPlayer() {
return player;
}
}
Loading