Skip to content

Commit de5cc87

Browse files
committed
suppress pre-join login dialog via config-phase auto-login
1 parent c865c70 commit de5cc87

11 files changed

Lines changed: 366 additions & 37 deletions

File tree

authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java

Lines changed: 98 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import javax.inject.Inject;
2323
import java.util.List;
24+
import java.util.Locale;
2425
import java.util.Optional;
2526
import java.util.UUID;
2627

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

4344
private boolean isEnabled;
4445
private String proxySharedSecret;
46+
private boolean channelRegistered;
4547

4648
@Inject
4749
BungeeReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager,
@@ -62,12 +64,17 @@ public void reload(Settings settings) {
6264
this.proxySharedSecret = settings.getProperty(HooksSettings.PROXY_SHARED_SECRET);
6365
this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD);
6466
final Messenger messenger = plugin.getServer().getMessenger();
65-
if (this.isEnabled && messenger != null) {
66-
if (!messenger.isIncomingChannelRegistered(plugin, AUTHME_CHANNEL)) {
67-
messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this);
68-
}
69-
} else if (messenger != null && messenger.isIncomingChannelRegistered(plugin, AUTHME_CHANNEL)) {
67+
if (messenger == null) {
68+
return;
69+
}
70+
// Track our own registration rather than querying the channel: other listeners
71+
// (e.g. the Paper configuration-phase receiver) may register the same channel.
72+
if (this.isEnabled && !channelRegistered) {
73+
messenger.registerIncomingPluginChannel(plugin, AUTHME_CHANNEL, this);
74+
channelRegistered = true;
75+
} else if (!this.isEnabled && channelRegistered) {
7076
messenger.unregisterIncomingPluginChannel(plugin, AUTHME_CHANNEL, this);
77+
channelRegistered = false;
7178
}
7279
}
7380

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

128135
if (type.get() == MessageType.PERFORM_LOGIN) {
129-
long timestamp;
130-
String uuidOrHmac;
131-
UUID verifiedPremiumUuid = null;
132-
String hmac;
133-
try {
134-
timestamp = in.readLong();
135-
uuidOrHmac = in.readUTF();
136-
} catch (IllegalStateException e) {
137-
logger.warning("Received perform.login without HMAC — update your proxy plugin");
136+
VerifiedProxyLogin verified = parseAndVerifyPerformLogin(in, argument);
137+
if (verified == null) {
138138
return;
139139
}
140-
try {
141-
UUID parsedUuid = UuidUtils.parseUuidSafely(uuidOrHmac);
142-
if (parsedUuid != null || uuidOrHmac.isEmpty()) {
143-
verifiedPremiumUuid = parsedUuid;
144-
hmac = in.readUTF();
145-
} else {
146-
hmac = uuidOrHmac;
147-
}
148-
} catch (IllegalStateException e) {
140+
performLogin(verified.name, verified.verifiedPremiumUuid);
141+
}
142+
}
143+
144+
/**
145+
* Parses and HMAC-verifies the remainder of a {@code perform.login} message (everything after the
146+
* player-name argument).
147+
*
148+
* @param in the data input positioned right after the player-name argument
149+
* @param playerName the player-name argument that was already read
150+
* @return the verified login data, or {@code null} if the message was malformed or failed verification
151+
*/
152+
private VerifiedProxyLogin parseAndVerifyPerformLogin(ByteArrayDataInput in, String playerName) {
153+
long timestamp;
154+
String uuidOrHmac;
155+
UUID verifiedPremiumUuid = null;
156+
String hmac;
157+
try {
158+
timestamp = in.readLong();
159+
uuidOrHmac = in.readUTF();
160+
} catch (IllegalStateException e) {
161+
logger.warning("Received perform.login without HMAC — update your proxy plugin");
162+
return null;
163+
}
164+
try {
165+
UUID parsedUuid = UuidUtils.parseUuidSafely(uuidOrHmac);
166+
if (parsedUuid != null || uuidOrHmac.isEmpty()) {
167+
verifiedPremiumUuid = parsedUuid;
168+
hmac = in.readUTF();
169+
} else {
149170
hmac = uuidOrHmac;
150171
}
151-
if (!verifyHmac(argument, timestamp, verifiedPremiumUuid, hmac)) {
152-
return;
172+
} catch (IllegalStateException e) {
173+
hmac = uuidOrHmac;
174+
}
175+
if (!verifyHmac(playerName, timestamp, verifiedPremiumUuid, hmac)) {
176+
return null;
177+
}
178+
return new VerifiedProxyLogin(playerName, verifiedPremiumUuid);
179+
}
180+
181+
/**
182+
* Validates and queues a {@code perform.login} received during Paper/Folia's connection
183+
* configuration phase, when the player does not yet exist as a {@link Player}. Queuing it in the
184+
* {@link ProxySessionManager} lets the blocking pre-join login dialog be skipped (and
185+
* {@code processJoin} auto-login the player) instead of waiting for the post-join
186+
* {@code perform.login}, which arrives only after the configuration phase has completed.
187+
*
188+
* @param data the raw plugin-message payload
189+
* @return the normalized player name if this was a valid {@code perform.login}, otherwise {@code null}
190+
*/
191+
public String handleConfigPhasePerformLogin(byte[] data) {
192+
if (!isEnabled) {
193+
return null;
194+
}
195+
ByteArrayDataInput in = ByteStreams.newDataInput(data);
196+
String argument;
197+
try {
198+
String typeId = in.readUTF();
199+
if (!MessageType.PERFORM_LOGIN.getId().equals(typeId)) {
200+
return null;
153201
}
154-
performLogin(argument, verifiedPremiumUuid);
202+
argument = in.readUTF();
203+
} catch (IllegalStateException e) {
204+
return null;
205+
}
206+
VerifiedProxyLogin verified = parseAndVerifyPerformLogin(in, argument);
207+
if (verified == null) {
208+
return null;
155209
}
210+
proxySessionManager.processProxySessionMessage(verified.name, verified.verifiedPremiumUuid);
211+
logger.debug("Config-phase perform.login validated and queued for {0}", verified.name);
212+
return verified.name.toLowerCase(Locale.ROOT);
156213
}
157214

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

279+
/**
280+
* Holder for a validated {@code perform.login}: the player name and the proxy-verified premium UUID
281+
* (or {@code null} if the player is not a verified premium player).
282+
*/
283+
private static final class VerifiedProxyLogin {
284+
285+
private final String name;
286+
private final UUID verifiedPremiumUuid;
287+
288+
private VerifiedProxyLogin(String name, UUID verifiedPremiumUuid) {
289+
this.name = name;
290+
this.verifiedPremiumUuid = verifiedPremiumUuid;
291+
}
292+
}
293+
222294
}

authme-core/src/test/java/fr/xephi/authme/service/bungeecord/BungeeReceiverTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.google.common.io.ByteArrayDataOutput;
44
import com.google.common.io.ByteStreams;
55
import fr.xephi.authme.AuthMe;
6+
import fr.xephi.authme.TestHelper;
67
import fr.xephi.authme.data.ProxySessionManager;
78
import fr.xephi.authme.datasource.DataSource;
89
import fr.xephi.authme.process.Management;
@@ -14,6 +15,7 @@
1415
import org.bukkit.Server;
1516
import org.bukkit.entity.Player;
1617
import org.bukkit.plugin.messaging.Messenger;
18+
import org.junit.jupiter.api.BeforeAll;
1719
import org.junit.jupiter.api.BeforeEach;
1820
import org.junit.jupiter.api.Test;
1921
import org.junit.jupiter.api.extension.ExtendWith;
@@ -26,6 +28,9 @@
2628

2729
import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToRunTaskAsynchronously;
2830
import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToScheduleSyncTaskFromOptionallyAsyncTask;
31+
import static org.hamcrest.MatcherAssert.assertThat;
32+
import static org.hamcrest.Matchers.equalTo;
33+
import static org.hamcrest.Matchers.nullValue;
2934
import static org.mockito.ArgumentMatchers.any;
3035
import static org.mockito.ArgumentMatchers.eq;
3136
import static org.mockito.BDDMockito.given;
@@ -67,6 +72,11 @@ class BungeeReceiverTest {
6772
@Mock
6873
private Messenger messenger;
6974

75+
@BeforeAll
76+
static void initLogger() {
77+
TestHelper.setupLogger();
78+
}
79+
7080
@BeforeEach
7181
void setUp() {
7282
given(plugin.getServer()).willReturn(server);
@@ -228,6 +238,56 @@ void shouldRemoveQueuedRequestWhenPremiumValidateRejects() {
228238
verify(bungeeSender, never()).sendAuthMeBungeecordMessage(any(), any());
229239
}
230240

241+
@Test
242+
void shouldValidateAndQueueConfigPhasePerformLogin() {
243+
// given
244+
String sharedSecret = "test-secret";
245+
String playerName = "Bobby";
246+
long timestamp = System.currentTimeMillis();
247+
String hmac = HashUtils.hmacSha256(sharedSecret, playerName + ":" + timestamp + ":");
248+
249+
given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true);
250+
given(settings.getProperty(HooksSettings.PROXY_SHARED_SECRET)).willReturn(sharedSecret);
251+
252+
BungeeReceiver receiver =
253+
new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource,
254+
proxyLoginRequestValidator, settings);
255+
256+
byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac);
257+
258+
// when
259+
String result = receiver.handleConfigPhasePerformLogin(payload);
260+
261+
// then
262+
assertThat(result, equalTo("bobby"));
263+
verify(proxySessionManager).processProxySessionMessage(playerName, null);
264+
verify(management, never()).forceLoginFromProxy(any());
265+
}
266+
267+
@Test
268+
void shouldRejectConfigPhasePerformLoginWithInvalidHmac() {
269+
// given
270+
String sharedSecret = "test-secret";
271+
String playerName = "Bobby";
272+
long timestamp = System.currentTimeMillis();
273+
274+
given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true);
275+
given(settings.getProperty(HooksSettings.PROXY_SHARED_SECRET)).willReturn(sharedSecret);
276+
277+
BungeeReceiver receiver =
278+
new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource,
279+
proxyLoginRequestValidator, settings);
280+
281+
byte[] payload = buildPerformLoginPayload(playerName, timestamp, "not-a-valid-hmac");
282+
283+
// when
284+
String result = receiver.handleConfigPhasePerformLogin(payload);
285+
286+
// then
287+
assertThat(result, nullValue());
288+
verify(proxySessionManager, never()).processProxySessionMessage(any(), any());
289+
}
290+
231291
private static byte[] buildPerformLoginPayload(String playerName, long timestamp, String hmac) {
232292
return buildPerformLoginPayload(playerName, timestamp, "", hmac);
233293
}

authme-folia/src/main/java/fr/xephi/authme/platform/FoliaPlatformAdapter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import fr.xephi.authme.listener.PaperLoginValidationListener;
66
import fr.xephi.authme.listener.FoliaPlayerSpawnLocationListener;
77
import fr.xephi.authme.listener.PaperDialogFlowListener;
8+
import fr.xephi.authme.listener.PaperProxyAutoLoginListener;
89
import fr.xephi.authme.listener.PlayerOpenSignListener;
910
import fr.xephi.authme.service.CancellableTask;
1011
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
@@ -108,6 +109,7 @@ public List<Class<? extends Listener>> getListeners() {
108109
Arrays.asList(
109110
FoliaChatListener.class,
110111
PaperDialogFlowListener.class,
112+
PaperProxyAutoLoginListener.class,
111113
FoliaPlayerSpawnLocationListener.class,
112114
PaperLoginValidationListener.class,
113115
PlayerOpenSignListener.class));

authme-folia/src/test/java/fr/xephi/authme/platform/FoliaPlatformAdapterTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import fr.xephi.authme.listener.FoliaChatListener;
66
import fr.xephi.authme.listener.FoliaPlayerSpawnLocationListener;
77
import fr.xephi.authme.listener.PaperDialogFlowListener;
8+
import fr.xephi.authme.listener.PaperProxyAutoLoginListener;
89
import fr.xephi.authme.listener.PaperLoginValidationListener;
910
import fr.xephi.authme.listener.PlayerListener;
1011
import fr.xephi.authme.listener.PlayerOpenSignListener;
@@ -76,6 +77,7 @@ public void getListenersContainsCoreAndFoliaListeners() {
7677
ServerListener.class,
7778
FoliaChatListener.class,
7879
PaperDialogFlowListener.class,
80+
PaperProxyAutoLoginListener.class,
7981
FoliaPlayerSpawnLocationListener.class,
8082
PaperLoginValidationListener.class,
8183
PlayerOpenSignListener.class));

authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,21 @@ private void handleBlockingLoginDialog(PlayerConfigurationConnection connection,
199199
long timeoutSeconds = Math.max(commonService.getProperty(RestrictionSettings.LOGIN_TIMEOUT), 1);
200200
loginResponse.completeOnTimeout(
201201
messages.retrieveSingle(playerName, MessageKey.LOGIN_TIMEOUT_ERROR), timeoutSeconds, TimeUnit.SECONDS);
202+
String normalizedName = playerName.toLowerCase(Locale.ROOT);
202203
pendingLoginResponses.put(playerId, loginResponse);
203-
preJoinDialogService.registerPreJoinFuture(playerName.toLowerCase(java.util.Locale.ROOT), playerId, loginResponse);
204+
preJoinDialogService.registerPreJoinFuture(normalizedName, playerId, loginResponse);
205+
206+
// Close the race with a proxy auto-login (perform.login) that arrived during the configuration
207+
// phase between the shouldSkipDialogs() check and now: if a proxy session has been queued,
208+
// force-login instead of showing the dialog.
209+
if (proxySessionManager.shouldResumeSession(normalizedName)) {
210+
preJoinDialogService.approvePreJoinForceLogin(normalizedName);
211+
}
204212

205-
connection.getAudience().showDialog(
206-
PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName)));
213+
if (!loginResponse.isDone()) {
214+
connection.getAudience().showDialog(
215+
PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName)));
216+
}
207217
String kickMessage = loginResponse.join();
208218
pendingLoginResponses.remove(playerId);
209219
preJoinDialogService.unregisterPreJoinFuture(playerId);

0 commit comments

Comments
 (0)