Skip to content
Merged
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 @@ -21,6 +21,7 @@

import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;

Expand All @@ -42,6 +43,7 @@ public class BungeeReceiver implements PluginMessageListener, SettingsDependent

private boolean isEnabled;
private String proxySharedSecret;
private boolean channelRegistered;

@Inject
BungeeReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager,
Expand All @@ -62,12 +64,17 @@ public void reload(Settings settings) {
this.proxySharedSecret = settings.getProperty(HooksSettings.PROXY_SHARED_SECRET);
this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD);
final Messenger messenger = plugin.getServer().getMessenger();
if (this.isEnabled && messenger != null) {
if (!messenger.isIncomingChannelRegistered(plugin, AUTHME_CHANNEL)) {
messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this);
}
} else if (messenger != null && messenger.isIncomingChannelRegistered(plugin, AUTHME_CHANNEL)) {
if (messenger == null) {
return;
}
// Track our own registration rather than querying the channel: other listeners
// (e.g. the Paper configuration-phase receiver) may register the same channel.
if (this.isEnabled && !channelRegistered) {
messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this);
channelRegistered = true;
} else if (!this.isEnabled && channelRegistered) {
messenger.unregisterIncomingPluginChannel(plugin, AUTHME_CHANNEL, this);
channelRegistered = false;
}
}

Expand Down Expand Up @@ -126,33 +133,83 @@ public void onPluginMessageReceived(String channel, Player player, byte[] data)
}

if (type.get() == MessageType.PERFORM_LOGIN) {
long timestamp;
String uuidOrHmac;
UUID verifiedPremiumUuid = null;
String hmac;
try {
timestamp = in.readLong();
uuidOrHmac = in.readUTF();
} catch (IllegalStateException e) {
logger.warning("Received perform.login without HMAC — update your proxy plugin");
VerifiedProxyLogin verified = parseAndVerifyPerformLogin(in, argument);
if (verified == null) {
return;
}
try {
UUID parsedUuid = UuidUtils.parseUuidSafely(uuidOrHmac);
if (parsedUuid != null || uuidOrHmac.isEmpty()) {
verifiedPremiumUuid = parsedUuid;
hmac = in.readUTF();
} else {
hmac = uuidOrHmac;
}
} catch (IllegalStateException e) {
performLogin(verified.name, verified.verifiedPremiumUuid);
}
}

/**
* Parses and HMAC-verifies the remainder of a {@code perform.login} message (everything after the
* player-name argument).
*
* @param in the data input positioned right after the player-name argument
* @param playerName the player-name argument that was already read
* @return the verified login data, or {@code null} if the message was malformed or failed verification
*/
private VerifiedProxyLogin parseAndVerifyPerformLogin(ByteArrayDataInput in, String playerName) {
long timestamp;
String uuidOrHmac;
UUID verifiedPremiumUuid = null;
String hmac;
try {
timestamp = in.readLong();
uuidOrHmac = in.readUTF();
} catch (IllegalStateException e) {
logger.warning("Received perform.login without HMAC — update your proxy plugin");
return null;
}
try {
UUID parsedUuid = UuidUtils.parseUuidSafely(uuidOrHmac);
if (parsedUuid != null || uuidOrHmac.isEmpty()) {
verifiedPremiumUuid = parsedUuid;
hmac = in.readUTF();
} else {
hmac = uuidOrHmac;
}
if (!verifyHmac(argument, timestamp, verifiedPremiumUuid, hmac)) {
return;
} catch (IllegalStateException e) {
hmac = uuidOrHmac;
}
if (!verifyHmac(playerName, timestamp, verifiedPremiumUuid, hmac)) {
return null;
}
return new VerifiedProxyLogin(playerName, verifiedPremiumUuid);
}

/**
* Validates and queues a {@code perform.login} received during Paper/Folia's connection
* configuration phase, when the player does not yet exist as a {@link Player}. Queuing it in the
* {@link ProxySessionManager} lets the blocking pre-join login dialog be skipped (and
* {@code processJoin} auto-login the player) instead of waiting for the post-join
* {@code perform.login}, which arrives only after the configuration phase has completed.
*
* @param data the raw plugin-message payload
* @return the normalized player name if this was a valid {@code perform.login}, otherwise {@code null}
*/
public String handleConfigPhasePerformLogin(byte[] data) {
if (!isEnabled) {
return null;
}
ByteArrayDataInput in = ByteStreams.newDataInput(data);
String argument;
try {
String typeId = in.readUTF();
if (!MessageType.PERFORM_LOGIN.getId().equals(typeId)) {
return null;
}
performLogin(argument, verifiedPremiumUuid);
argument = in.readUTF();
} catch (IllegalStateException e) {
return null;
}
VerifiedProxyLogin verified = parseAndVerifyPerformLogin(in, argument);
if (verified == null) {
return null;
}
proxySessionManager.processProxySessionMessage(verified.name, verified.verifiedPremiumUuid);
logger.debug("Config-phase perform.login validated and queued for {0}", verified.name);
return verified.name.toLowerCase(Locale.ROOT);
}

private boolean verifyHmac(String playerName, long timestamp, UUID verifiedPremiumUuid, String providedHmac) {
Expand Down Expand Up @@ -219,4 +276,19 @@ private void completeProxyLogin(Player player) {
logger.info(player.getName() + " has been automatically logged in via proxy request.");
}

/**
* Holder for a validated {@code perform.login}: the player name and the proxy-verified premium UUID
* (or {@code null} if the player is not a verified premium player).
*/
private static final class VerifiedProxyLogin {

private final String name;
private final UUID verifiedPremiumUuid;

private VerifiedProxyLogin(String name, UUID verifiedPremiumUuid) {
this.name = name;
this.verifiedPremiumUuid = verifiedPremiumUuid;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.ProxySessionManager;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.process.Management;
Expand All @@ -14,6 +15,7 @@
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.Messenger;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -26,6 +28,9 @@

import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToRunTaskAsynchronously;
import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToScheduleSyncTaskFromOptionallyAsyncTask;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
Expand Down Expand Up @@ -67,6 +72,11 @@ class BungeeReceiverTest {
@Mock
private Messenger messenger;

@BeforeAll
static void initLogger() {
TestHelper.setupLogger();
}

@BeforeEach
void setUp() {
given(plugin.getServer()).willReturn(server);
Expand Down Expand Up @@ -228,6 +238,56 @@ void shouldRemoveQueuedRequestWhenPremiumValidateRejects() {
verify(bungeeSender, never()).sendAuthMeBungeecordMessage(any(), any());
}

@Test
void shouldValidateAndQueueConfigPhasePerformLogin() {
// given
String sharedSecret = "test-secret";
String playerName = "Bobby";
long timestamp = System.currentTimeMillis();
String hmac = HashUtils.hmacSha256(sharedSecret, playerName + ":" + timestamp + ":");

given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true);
given(settings.getProperty(HooksSettings.PROXY_SHARED_SECRET)).willReturn(sharedSecret);

BungeeReceiver receiver =
new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource,
proxyLoginRequestValidator, settings);

byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac);

// when
String result = receiver.handleConfigPhasePerformLogin(payload);

// then
assertThat(result, equalTo("bobby"));
verify(proxySessionManager).processProxySessionMessage(playerName, null);
verify(management, never()).forceLoginFromProxy(any());
}

@Test
void shouldRejectConfigPhasePerformLoginWithInvalidHmac() {
// given
String sharedSecret = "test-secret";
String playerName = "Bobby";
long timestamp = System.currentTimeMillis();

given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true);
given(settings.getProperty(HooksSettings.PROXY_SHARED_SECRET)).willReturn(sharedSecret);

BungeeReceiver receiver =
new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource,
proxyLoginRequestValidator, settings);

byte[] payload = buildPerformLoginPayload(playerName, timestamp, "not-a-valid-hmac");

// when
String result = receiver.handleConfigPhasePerformLogin(payload);

// then
assertThat(result, nullValue());
verify(proxySessionManager, never()).processProxySessionMessage(any(), any());
}

private static byte[] buildPerformLoginPayload(String playerName, long timestamp, String hmac) {
return buildPerformLoginPayload(playerName, timestamp, "", hmac);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import fr.xephi.authme.listener.PaperLoginValidationListener;
import fr.xephi.authme.listener.FoliaPlayerSpawnLocationListener;
import fr.xephi.authme.listener.PaperDialogFlowListener;
import fr.xephi.authme.listener.PaperProxyAutoLoginListener;
import fr.xephi.authme.listener.PlayerOpenSignListener;
import fr.xephi.authme.service.CancellableTask;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
Expand Down Expand Up @@ -108,6 +109,7 @@ public List<Class<? extends Listener>> getListeners() {
Arrays.asList(
FoliaChatListener.class,
PaperDialogFlowListener.class,
PaperProxyAutoLoginListener.class,
FoliaPlayerSpawnLocationListener.class,
PaperLoginValidationListener.class,
PlayerOpenSignListener.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import fr.xephi.authme.listener.FoliaChatListener;
import fr.xephi.authme.listener.FoliaPlayerSpawnLocationListener;
import fr.xephi.authme.listener.PaperDialogFlowListener;
import fr.xephi.authme.listener.PaperProxyAutoLoginListener;
import fr.xephi.authme.listener.PaperLoginValidationListener;
import fr.xephi.authme.listener.PlayerListener;
import fr.xephi.authme.listener.PlayerOpenSignListener;
Expand Down Expand Up @@ -76,6 +77,7 @@ public void getListenersContainsCoreAndFoliaListeners() {
ServerListener.class,
FoliaChatListener.class,
PaperDialogFlowListener.class,
PaperProxyAutoLoginListener.class,
FoliaPlayerSpawnLocationListener.class,
PaperLoginValidationListener.class,
PlayerOpenSignListener.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,21 @@ private void handleBlockingLoginDialog(PlayerConfigurationConnection connection,
long timeoutSeconds = Math.max(commonService.getProperty(RestrictionSettings.LOGIN_TIMEOUT), 1);
loginResponse.completeOnTimeout(
messages.retrieveSingle(playerName, MessageKey.LOGIN_TIMEOUT_ERROR), timeoutSeconds, TimeUnit.SECONDS);
String normalizedName = playerName.toLowerCase(Locale.ROOT);
pendingLoginResponses.put(playerId, loginResponse);
preJoinDialogService.registerPreJoinFuture(playerName.toLowerCase(java.util.Locale.ROOT), playerId, loginResponse);
preJoinDialogService.registerPreJoinFuture(normalizedName, playerId, loginResponse);

// Close the race with a proxy auto-login (perform.login) that arrived during the configuration
// phase between the shouldSkipDialogs() check and now: if a proxy session has been queued,
// force-login instead of showing the dialog.
if (proxySessionManager.shouldResumeSession(normalizedName)) {
preJoinDialogService.approvePreJoinForceLogin(normalizedName);
}

connection.getAudience().showDialog(
PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName)));
if (!loginResponse.isDone()) {
connection.getAudience().showDialog(
PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName)));
}
String kickMessage = loginResponse.join();
pendingLoginResponses.remove(playerId);
preJoinDialogService.unregisterPreJoinFuture(playerId);
Expand Down
Loading
Loading