From ffeda2db59a00541b0592cacd33af626fe67d68b Mon Sep 17 00:00:00 2001 From: Xephi Date: Sat, 2 May 2026 16:50:11 +0200 Subject: [PATCH 1/3] fix(proxy): Correctly perform autoLogin, even if the user is online --- .../authme/process/join/AsynchronousJoin.java | 5 +- .../service/bungeecord/BungeeReceiver.java | 7 +- .../process/join/AsynchronousJoinTest.java | 19 +++++ .../bungeecord/BungeeReceiverTest.java | 73 +++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 0e2d165e0..0b81d0840 100644 --- a/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -176,7 +176,10 @@ public void processJoin(Player player) { // Run commands bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(player, () -> commandManager.runCommandsOnSessionLogin(player)); - bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); + // Use forceLoginFromProxy (quiet=true, no BungeeCord redirect) so that if + // BungeeReceiver.performLogin() concurrently already completed the login, this + // call is a no-op rather than sending an "already logged in" error. + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLoginFromProxy(player)); logger.info("The user " + player.getName() + " has been automatically logged in, " + "as present in autologin queue."); return; 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 598652bd5..4707e2983 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 @@ -129,14 +129,19 @@ private boolean verifyHmac(String playerName, long timestamp, String providedHma private void performLogin(String name) { logger.debug("Received perform.login request for " + name); + // Always queue in the proxy session manager so processJoin can consume it even when + // the player is already online (PlayerJoinEvent fires before ServerSwitchEvent on the + // proxy, so processJoin may run before perform.login arrives at this backend). + proxySessionManager.processProxySessionMessage(name); Player player = bukkitService.getPlayerExact(name); if (player != null && player.isOnline()) { + // Player is already online: also drive the login directly in case processJoin + // has already run past the proxy-session check and created a limbo player. management.forceLoginFromProxy(player); logger.debug("Sending auto-login ACK for " + player.getName()); bungeeSender.sendAuthMeBungeecordMessage(player, MessageType.PERFORM_LOGIN_ACK); logger.info(player.getName() + " has been automatically logged in via proxy request."); } else { - proxySessionManager.processProxySessionMessage(name); logger.info(name + " is not yet online; queued for auto-login when they connect."); } } diff --git a/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java b/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java index bacc00455..e77c70516 100644 --- a/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java @@ -183,6 +183,25 @@ public void shouldResumeSessionWithoutOpeningDialog() { verify(dialogAdapter, never()).showLoginDialog(eq(player), any(DialogWindowSpec.class)); } + @Test + public void shouldAutoLoginFromProxySessionWithoutCreatingLimbo() { + // given + Player player = mockPlayer("Bobby"); + setUpRegisteredJoin(player); + given(proxySessionManager.shouldResumeSession("bobby")).willReturn(true); + + // when + asynchronousJoin.processJoin(player); + + // then - uses forceLoginFromProxy (quiet, no redirect) so a concurrent BungeeReceiver + // login does not cause an "already logged in" error + verify(service).send(player, fr.xephi.authme.message.MessageKey.SESSION_RECONNECTION); + verify(commandManager).runCommandsOnSessionLogin(player); + verify(asynchronousLogin).forceLoginFromProxy(player); + verify(asynchronousLogin, never()).forceLogin(player); + verify(limboService, never()).createLimboPlayer(player, true); + } + @Test public void shouldProcessPendingPreJoinLoginInsteadOfShowingDialog() { // given 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 e1ddb70f5..037180574 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 @@ -1,12 +1,16 @@ package fr.xephi.authme.service.bungeecord; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; import fr.xephi.authme.AuthMe; import fr.xephi.authme.data.ProxySessionManager; import fr.xephi.authme.process.Management; +import fr.xephi.authme.security.HashUtils; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import org.bukkit.Server; +import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.Messenger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,6 +23,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -77,4 +83,71 @@ void shouldUnregisterIncomingChannelWhenDisabledOnReload() { verify(messenger).registerIncomingPluginChannel(plugin, "authme:main", bungeeReceiver); verify(messenger).unregisterIncomingPluginChannel(plugin, "authme:main", bungeeReceiver); } + + @Test + void shouldQueueSessionAndForceLoginWhenPerformLoginReceivedForOnlinePlayer() { + // 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); + given(messenger.isIncomingChannelRegistered(plugin, "authme:main")).willReturn(false); + + Player player = mock(Player.class); + given(player.isOnline()).willReturn(true); + given(bukkitService.getPlayerExact(playerName)).willReturn(player); + + BungeeReceiver receiver = + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, settings); + + byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac); + + // when + receiver.onPluginMessageReceived("authme:main", player, payload); + + // then + verify(proxySessionManager).processProxySessionMessage(playerName); + verify(management).forceLoginFromProxy(player); + verify(bungeeSender).sendAuthMeBungeecordMessage(player, MessageType.PERFORM_LOGIN_ACK); + } + + @Test + void shouldOnlyQueueSessionWhenPerformLoginReceivedForOfflinePlayer() { + // 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); + given(messenger.isIncomingChannelRegistered(plugin, "authme:main")).willReturn(false); + given(bukkitService.getPlayerExact(playerName)).willReturn(null); + + BungeeReceiver receiver = + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, settings); + + Player carrier = mock(Player.class); + byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac); + + // when + receiver.onPluginMessageReceived("authme:main", carrier, payload); + + // then + verify(proxySessionManager).processProxySessionMessage(playerName); + verify(management, never()).forceLoginFromProxy(any()); + verify(bungeeSender, never()).sendAuthMeBungeecordMessage(any(), any()); + } + + private static byte[] buildPerformLoginPayload(String playerName, long timestamp, String hmac) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF(MessageType.PERFORM_LOGIN.getId()); + out.writeUTF(playerName); + out.writeLong(timestamp); + out.writeUTF(hmac); + return out.toByteArray(); + } } From 5a11c6fc891f6a664b605f62412571f50cf96be4 Mon Sep 17 00:00:00 2001 From: Xephi Date: Sun, 3 May 2026 01:07:57 +0200 Subject: [PATCH 2/3] fix: admin force login commands not clean/skip Dialog correctly for the user --- .../executable/authme/ForceLoginCommand.java | 14 ++- .../authme/process/join/AsynchronousJoin.java | 9 +- .../authme/service/PreJoinDialogService.java | 65 ++++++++++++++ .../authme/ForceLoginCommandTest.java | 38 ++++++++ .../process/join/AsynchronousJoinTest.java | 19 ++++ .../service/PreJoinDialogServiceTest.java | 86 +++++++++++++++++++ .../listener/PaperDialogFlowListener.java | 2 + 7 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 authme-core/src/test/java/fr/xephi/authme/service/PreJoinDialogServiceTest.java diff --git a/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java b/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java index 7895ff12b..0f063c7ac 100644 --- a/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java +++ b/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java @@ -6,11 +6,13 @@ import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PreJoinDialogService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import javax.inject.Inject; import java.util.List; +import java.util.Locale; import static fr.xephi.authme.permission.PlayerPermission.CAN_LOGIN_BE_FORCED; @@ -31,13 +33,23 @@ public class ForceLoginCommand implements ExecutableCommand { @Inject private Messages messages; + @Inject + private PreJoinDialogService preJoinDialogService; + @Override public void executeCommand(CommandSender sender, List arguments) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); Player player = bukkitService.getPlayerExact(playerName); if (player == null || !player.isOnline()) { - messages.send(sender, MessageKey.FORCE_LOGIN_PLAYER_OFFLINE); + // Player may be blocked in the pre-join dialog (Paper/Folia configuration phase). + // Approving the force-login completes the blocking future so the player proceeds to + // PLAY state, where AsynchronousJoin will call forceLogin() on their behalf. + if (preJoinDialogService.approvePreJoinForceLogin(playerName.toLowerCase(Locale.ROOT))) { + messages.send(sender, MessageKey.FORCE_LOGIN_SUCCESS, playerName); + } else { + messages.send(sender, MessageKey.FORCE_LOGIN_PLAYER_OFFLINE); + } } else if (!permissionsManager.hasPermission(player, CAN_LOGIN_BE_FORCED)) { messages.send(sender, MessageKey.FORCE_LOGIN_FORBIDDEN, playerName); } else { diff --git a/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 0b81d0840..528f5a8d0 100644 --- a/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -126,6 +126,7 @@ public void processJoin(Player player) { PreJoinDialogService.PendingRegistration pendingRegistration = preJoinDialogService.consumePendingRegistration(playerId); boolean shouldSkipPostJoinDialog = preJoinDialogService.consumeSkipPostJoinDialog(playerId); + boolean pendingForceLogin = preJoinDialogService.consumePendingForceLogin(playerId); if (!validationService.fulfillsNameRestrictions(player)) { handlePlayerWithUnmetNameRestriction(player, ip); @@ -200,7 +201,7 @@ public void processJoin(Player player) { return; } - processJoinSync(player, isAuthAvailable, pendingLoginPassword, pendingRegistration, shouldSkipPostJoinDialog); + processJoinSync(player, isAuthAvailable, pendingLoginPassword, pendingRegistration, shouldSkipPostJoinDialog, pendingForceLogin); } private void handlePlayerWithUnmetNameRestriction(Player player, String ip) { @@ -220,7 +221,7 @@ private void handlePlayerWithUnmetNameRestriction(Player player, String ip) { */ private void processJoinSync(Player player, boolean isAuthAvailable, String pendingLoginPassword, PreJoinDialogService.PendingRegistration pendingRegistration, - boolean shouldSkipPostJoinDialog) { + boolean shouldSkipPostJoinDialog, boolean pendingForceLogin) { int registrationTimeout = service.getProperty( isAuthAvailable ? RestrictionSettings.LOGIN_TIMEOUT : RestrictionSettings.REGISTER_TIMEOUT ) * TICKS_PER_SECOND; @@ -246,6 +247,10 @@ private void processJoinSync(Player player, boolean isAuthAvailable, String pend bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.login(player, pendingLoginPassword)); return; } + if (pendingForceLogin) { + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); + return; + } if (!shouldSkipPostJoinDialog && !playerCache.isAuthenticated(player.getName()) && service.getProperty(RegistrationSettings.USE_DIALOG_UI) diff --git a/authme-core/src/main/java/fr/xephi/authme/service/PreJoinDialogService.java b/authme-core/src/main/java/fr/xephi/authme/service/PreJoinDialogService.java index 50db5a507..b6a6a76f0 100644 --- a/authme-core/src/main/java/fr/xephi/authme/service/PreJoinDialogService.java +++ b/authme-core/src/main/java/fr/xephi/authme/service/PreJoinDialogService.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; /** @@ -14,6 +15,12 @@ public class PreJoinDialogService { private final Map pendingRegistrations = new ConcurrentHashMap<>(); private final Set skipPostJoinDialogs = ConcurrentHashMap.newKeySet(); + // Pre-join force-login: tracks players blocked in the pre-join login dialog so that + // ForceLoginCommand can unblock them without requiring the player to be in PLAY state. + private final Map pendingPreJoinByName = new ConcurrentHashMap<>(); + private final Map> pendingPreJoinFutures = new ConcurrentHashMap<>(); + private final Set pendingForceLogins = ConcurrentHashMap.newKeySet(); + public PreJoinDialogService() { } @@ -45,10 +52,68 @@ public boolean consumeSkipPostJoinDialog(UUID playerId) { return skipPostJoinDialogs.remove(playerId); } + /** + * Registers the blocking {@link CompletableFuture} used by the pre-join login dialog so that + * {@link #approvePreJoinForceLogin} can resolve it from outside the event handler thread. + * + * @param normalizedName the player name in lowercase + * @param uuid the player's UUID + * @param future the future that blocks the configuration-phase thread + */ + public void registerPreJoinFuture(String normalizedName, UUID uuid, CompletableFuture future) { + pendingPreJoinByName.put(normalizedName, uuid); + pendingPreJoinFutures.put(uuid, future); + } + + /** + * Removes the pre-join future registration once the blocking wait is over. + * + * @param uuid the player's UUID + */ + public void unregisterPreJoinFuture(UUID uuid) { + pendingPreJoinFutures.remove(uuid); + pendingPreJoinByName.values().remove(uuid); + } + + /** + * Approves a force-login for a player currently blocked in the pre-join login dialog. + * Completes the blocking future with {@code null} (no kick message), allowing the player to + * proceed to PLAY state where {@link #consumePendingForceLogin} will trigger a force-login. + * + * @param normalizedName the player name in lowercase + * @return {@code true} if the player was in the pre-join dialog and the approval was registered, + * {@code false} if no such player was found (e.g. already joined or not in dialog) + */ + public boolean approvePreJoinForceLogin(String normalizedName) { + UUID uuid = pendingPreJoinByName.get(normalizedName); + if (uuid == null) { + return false; + } + CompletableFuture future = pendingPreJoinFutures.get(uuid); + if (future == null) { + return false; + } + pendingForceLogins.add(uuid); + future.complete(null); + return true; + } + + /** + * Consumes the force-login flag for the given player. + * + * @param playerId the player's UUID + * @return {@code true} if a force-login was approved for this player (flag is cleared) + */ + public boolean consumePendingForceLogin(UUID playerId) { + return pendingForceLogins.remove(playerId); + } + public void clear(UUID playerId) { pendingLoginPasswords.remove(playerId); pendingRegistrations.remove(playerId); skipPostJoinDialogs.remove(playerId); + pendingForceLogins.remove(playerId); + unregisterPreJoinFuture(playerId); } public record PendingRegistration(String primaryValue, String secondaryValue, boolean isEmailRegistration) { diff --git a/authme-core/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java b/authme-core/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java index 66dab60d7..02d3506d8 100644 --- a/authme-core/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java @@ -6,6 +6,7 @@ import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PreJoinDialogService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.jupiter.api.Test; @@ -43,6 +44,9 @@ class ForceLoginCommandTest { @Mock private Messages messages; + @Mock + private PreJoinDialogService preJoinDialogService; + @Test void shouldRejectOfflinePlayer() { // given @@ -131,6 +135,40 @@ void shouldForceLoginSenderSelf() { verify(messages).send(eq(sender), eq(MessageKey.FORCE_LOGIN_SUCCESS), eq(senderName)); } + @Test + void shouldForceLoginPlayerBlockedInPreJoinDialog() { + // given + String playerName = "Connor"; + given(bukkitService.getPlayerExact(playerName)).willReturn(null); + given(preJoinDialogService.approvePreJoinForceLogin("connor")).willReturn(true); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(playerName)); + + // then + verify(preJoinDialogService).approvePreJoinForceLogin("connor"); + verify(messages).send(eq(sender), eq(MessageKey.FORCE_LOGIN_SUCCESS), eq(playerName)); + verifyNoInteractions(management); + } + + @Test + void shouldSendOfflineMessageWhenPlayerNotFoundAndNoPreJoinDialog() { + // given + String playerName = "NotConnecting"; + given(bukkitService.getPlayerExact(playerName)).willReturn(null); + given(preJoinDialogService.approvePreJoinForceLogin("notconnecting")).willReturn(false); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(playerName)); + + // then + verify(preJoinDialogService).approvePreJoinForceLogin("notconnecting"); + verify(messages).send(sender, MessageKey.FORCE_LOGIN_PLAYER_OFFLINE); + verifyNoInteractions(management); + } + private static Player mockPlayer(boolean isOnline) { Player player = mock(Player.class); given(player.isOnline()).willReturn(isOnline); diff --git a/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java b/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java index e77c70516..b33132193 100644 --- a/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java @@ -222,6 +222,25 @@ public void shouldProcessPendingPreJoinLoginInsteadOfShowingDialog() { verify(dialogAdapter, never()).showLoginDialog(eq(player), any(DialogWindowSpec.class)); } + @Test + public void shouldForceLoginPlayerApprovedViaPreJoinDialog() { + // given + Player player = mockPlayer("Bobby"); + setUpRegisteredJoin(player); + java.util.UUID playerId = java.util.UUID.randomUUID(); + given(player.getUniqueId()).willReturn(playerId); + given(preJoinDialogService.consumePendingForceLogin(playerId)).willReturn(true); + + // when + asynchronousJoin.processJoin(player); + + // then + verify(limboService).createLimboPlayer(player, true); + verify(asynchronousLogin).forceLogin(player); + verify(asynchronousLogin, never()).login(eq(player), any()); + verify(dialogAdapter, never()).showLoginDialog(eq(player), any()); + } + @Test public void shouldSkipPostJoinDialogWhenPreJoinDialogWasDeferred() { // given diff --git a/authme-core/src/test/java/fr/xephi/authme/service/PreJoinDialogServiceTest.java b/authme-core/src/test/java/fr/xephi/authme/service/PreJoinDialogServiceTest.java new file mode 100644 index 000000000..fc829bed5 --- /dev/null +++ b/authme-core/src/test/java/fr/xephi/authme/service/PreJoinDialogServiceTest.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.service; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +/** + * Tests for {@link PreJoinDialogService}. + */ +class PreJoinDialogServiceTest { + + @Test + void shouldStoreAndConsumePendingLoginPassword() { + PreJoinDialogService service = new PreJoinDialogService(); + UUID uuid = UUID.randomUUID(); + + service.storePendingLoginPassword(uuid, "s3cr3t"); + + assertThat(service.consumePendingLoginPassword(uuid), is("s3cr3t")); + assertThat(service.consumePendingLoginPassword(uuid), nullValue()); + } + + @Test + void shouldApprovePreJoinForceLoginAndCompleteFuture() { + PreJoinDialogService service = new PreJoinDialogService(); + UUID uuid = UUID.randomUUID(); + CompletableFuture future = new CompletableFuture<>(); + service.registerPreJoinFuture("bobby", uuid, future); + + boolean result = service.approvePreJoinForceLogin("bobby"); + + assertThat(result, is(true)); + assertThat(future.isDone(), is(true)); + assertThat(future.getNow("sentinel"), is(nullValue())); + assertThat(service.consumePendingForceLogin(uuid), is(true)); + assertThat(service.consumePendingForceLogin(uuid), is(false)); + } + + @Test + void shouldReturnFalseForApproveWhenNoPreJoinDialogPending() { + PreJoinDialogService service = new PreJoinDialogService(); + + assertThat(service.approvePreJoinForceLogin("nobody"), is(false)); + } + + @Test + void shouldReturnFalseForConsumeForceLoginWhenNotApproved() { + PreJoinDialogService service = new PreJoinDialogService(); + + assertThat(service.consumePendingForceLogin(UUID.randomUUID()), is(false)); + } + + @Test + void shouldNotApproveAfterUnregister() { + PreJoinDialogService service = new PreJoinDialogService(); + UUID uuid = UUID.randomUUID(); + CompletableFuture future = new CompletableFuture<>(); + service.registerPreJoinFuture("alice", uuid, future); + service.unregisterPreJoinFuture(uuid); + + boolean result = service.approvePreJoinForceLogin("alice"); + + assertThat(result, is(false)); + assertThat(future.isDone(), is(false)); + } + + @Test + void shouldClearAllStateForPlayer() { + PreJoinDialogService service = new PreJoinDialogService(); + UUID uuid = UUID.randomUUID(); + CompletableFuture future = new CompletableFuture<>(); + service.storePendingLoginPassword(uuid, "pw"); + service.registerPreJoinFuture("charlie", uuid, future); + + service.clear(uuid); + + assertThat(service.consumePendingLoginPassword(uuid), is(nullValue())); + assertThat(service.approvePreJoinForceLogin("charlie"), is(false)); + assertThat(service.consumePendingForceLogin(uuid), is(false)); + } +} diff --git a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java index 17c47175f..608522ac5 100644 --- a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java +++ b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java @@ -170,11 +170,13 @@ private void handleBlockingLoginDialog(PlayerConfigurationConnection connection, loginResponse.completeOnTimeout( messages.retrieveSingle(playerName, MessageKey.LOGIN_TIMEOUT_ERROR), timeoutSeconds, TimeUnit.SECONDS); pendingLoginResponses.put(playerId, loginResponse); + preJoinDialogService.registerPreJoinFuture(playerName.toLowerCase(java.util.Locale.ROOT), playerId, loginResponse); connection.getAudience().showDialog( PaperDialogHelper.createPreJoinLoginDialog(dialogWindowService.createPreJoinLoginDialog(playerName))); String kickMessage = loginResponse.join(); pendingLoginResponses.remove(playerId); + preJoinDialogService.unregisterPreJoinFuture(playerId); connection.getAudience().closeDialog(); if (kickMessage != null) { From 2cc2adb7e1dcfa42e4230ea64a4262010c2b9a1a Mon Sep 17 00:00:00 2001 From: Xephi Date: Sat, 2 May 2026 04:15:00 +0200 Subject: [PATCH 3/3] feat(premium): cryptographic Mojang session verification for premium bypass --- README.md | 16 +- .../authme/bungee/AuthMeBungeePlugin.java | 1 + .../authme/bungee/BungeeProxyBridge.java | 135 +++++++- .../bungee/BungeeProxyConfiguration.java | 9 +- .../bungee/config/BungeeConfigProperties.java | 7 + .../authme/bungee/BungeeProxyBridgeTest.java | 4 +- .../bungee/BungeeReloadCommandTest.java | 2 +- .../authme/command/CommandInitializer.java | 47 ++- .../authme/SetFreemiumAdminCommand.java | 22 ++ .../authme/SetPremiumAdminCommand.java | 22 ++ .../executable/premium/FreemiumCommand.java | 28 ++ .../executable/premium/PremiumCommand.java | 28 ++ .../fr/xephi/authme/data/auth/PlayerAuth.java | 20 ++ .../datasource/AbstractSqlDataSource.java | 16 + .../authme/datasource/CacheDataSource.java | 14 + .../fr/xephi/authme/datasource/Columns.java | 2 + .../xephi/authme/datasource/DataSource.java | 17 + .../fr/xephi/authme/datasource/MySQL.java | 8 + .../datasource/PostgreSqlDataSource.java | 10 + .../fr/xephi/authme/datasource/SQLite.java | 12 + .../columnshandler/AuthMeColumns.java | 5 + .../listener/packetevents/AesCfb8Decoder.java | 29 ++ .../listener/packetevents/AesCfb8Encoder.java | 28 ++ .../PacketEventsListenerRegistry.java | 21 ++ .../packetevents/PacketEventsService.java | 43 ++- .../PremiumVerificationPacketListener.java | 222 +++++++++++++ .../fr/xephi/authme/message/MessageKey.java | 56 +++- .../message/updater/MessageUpdater.java | 1 + .../authme/permission/AdminPermission.java | 12 +- .../authme/permission/PlayerPermission.java | 12 +- .../AbstractSpigotPlatformAdapter.java | 26 ++ .../platform/PacketInterceptionAdapter.java | 26 +- .../authme/process/join/AsynchronousJoin.java | 84 +++++ .../authme/service/MojangApiService.java | 124 +++++++ .../authme/service/PendingPremiumCache.java | 108 +++++++ .../authme/service/PremiumLoginVerifier.java | 209 ++++++++++++ .../xephi/authme/service/PremiumService.java | 302 ++++++++++++++++++ .../service/bungeecord/BungeeReceiver.java | 49 ++- .../service/bungeecord/BungeeSender.java | 74 ++++- .../service/bungeecord/MessageType.java | 6 +- .../properties/AuthMeSettingsRetriever.java | 2 +- .../settings/properties/DatabaseSettings.java | 5 + .../settings/properties/PremiumSettings.java | 27 ++ .../main/resources/messages/messages_bg.yml | 22 ++ .../main/resources/messages/messages_br.yml | 214 +++++++------ .../main/resources/messages/messages_cz.yml | 22 ++ .../main/resources/messages/messages_de.yml | 128 +++++--- .../main/resources/messages/messages_en.yml | 22 ++ .../main/resources/messages/messages_eo.yml | 22 ++ .../main/resources/messages/messages_es.yml | 22 ++ .../main/resources/messages/messages_et.yml | 140 ++++---- .../main/resources/messages/messages_eu.yml | 22 ++ .../main/resources/messages/messages_fi.yml | 22 ++ .../main/resources/messages/messages_fr.yml | 22 ++ .../main/resources/messages/messages_gl.yml | 22 ++ .../main/resources/messages/messages_hu.yml | 22 ++ .../main/resources/messages/messages_id.yml | 22 ++ .../main/resources/messages/messages_it.yml | 22 ++ .../main/resources/messages/messages_ja.yml | 22 ++ .../main/resources/messages/messages_ko.yml | 22 ++ .../main/resources/messages/messages_lt.yml | 22 ++ .../main/resources/messages/messages_nl.yml | 22 ++ .../main/resources/messages/messages_pl.yml | 22 ++ .../main/resources/messages/messages_pt.yml | 22 ++ .../main/resources/messages/messages_ro.yml | 22 ++ .../main/resources/messages/messages_ru.yml | 22 ++ .../main/resources/messages/messages_si.yml | 22 ++ .../main/resources/messages/messages_sk.yml | 22 ++ .../main/resources/messages/messages_sr.yml | 22 ++ .../main/resources/messages/messages_tr.yml | 22 ++ .../main/resources/messages/messages_uk.yml | 22 ++ .../main/resources/messages/messages_vn.yml | 22 ++ .../main/resources/messages/messages_zhcn.yml | 22 ++ .../main/resources/messages/messages_zhhk.yml | 22 ++ .../main/resources/messages/messages_zhmc.yml | 22 ++ .../main/resources/messages/messages_zhtw.yml | 22 ++ authme-core/src/main/resources/plugin.yml | 25 +- .../command/CommandInitializerTest.java | 2 +- .../AbstractSpigotPlatformAdapterTest.java | 11 + .../process/join/AsynchronousJoinTest.java | 23 +- .../bungeecord/BungeeReceiverTest.java | 20 +- .../authme/datasource/sql-initialize.sql | 3 +- authme-core/src/test/resources/plugin.yml | 25 +- authme-folia/src/main/resources/plugin.yml | 25 +- authme-paper-common/pom.xml | 5 + .../listener/PaperDialogFlowListener.java | 29 ++ .../AbstractPaperPlatformAdapter.java | 24 ++ .../listener/PaperDialogFlowListenerTest.java | 170 ++++++++++ authme-paper/src/main/resources/plugin.yml | 25 +- .../src/main/resources/plugin.yml | 25 +- .../src/main/resources/plugin.yml | 25 +- .../authme/velocity/AuthMeVelocityPlugin.java | 20 +- .../authme/velocity/VelocityProxyBridge.java | 122 ++++++- .../velocity/VelocityProxyConfiguration.java | 9 +- .../config/VelocityConfigProperties.java | 7 + .../velocity/VelocityProxyBridgeTest.java | 8 +- docs/commands.md | 14 +- docs/config.md | 16 +- docs/permission_nodes.md | 8 +- docs/premium.md | 192 +++++++++++ docs/proxies/bungee/config.yml | 5 +- docs/proxies/velocity/config.yml | 5 +- docs/translations.md | 16 +- 103 files changed, 3615 insertions(+), 277 deletions(-) create mode 100644 authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetFreemiumAdminCommand.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetPremiumAdminCommand.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/command/executable/premium/FreemiumCommand.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/command/executable/premium/PremiumCommand.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Decoder.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Encoder.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PremiumVerificationPacketListener.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/service/MojangApiService.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/service/PendingPremiumCache.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/service/PremiumLoginVerifier.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/service/PremiumService.java create mode 100644 authme-core/src/main/java/fr/xephi/authme/settings/properties/PremiumSettings.java create mode 100644 docs/premium.md diff --git a/README.md b/README.md index 360d94746..a94522ecd 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ You can also create your own translation file and, if you want, you can share it
  • Graphical login/register dialogs, with optional Paper/Folia pre-join dialogs
  • Restricted users (associate a username with an IP)
  • Protect player's inventory until correct authentication (requires PacketEvents)
  • +
  • Premium bypass: Mojang-account holders skip password auth (requires PacketEvents)
  • Saves the quit location of the player
  • Automatic database backup
  • Available languages: translations
  • @@ -75,6 +76,19 @@ AuthMe can display graphical login/register dialogs instead of chat-based prompt - `settings.registration.usePreJoinDialogUi` enables the **pre-join** dialog flow on **Paper/Folia**. - Both options are independent: you can enable either one, both, or neither. - Pre-join dialogs currently require modern dialog-capable server versions such as **Paper/Folia 1.21.11+**. +- Verified premium players skip the pre-join dialog entirely when premium bypass is enabled. + +#### Premium bypass +AuthMe can let players with a legitimate Mojang account skip password authentication entirely. +Identity is verified via a cryptographic handshake with Mojang's session server during the +Minecraft login phase — no password prompt is ever shown. + +- Enable with `settings.enablePremium: true` in `config.yml`. +- Players opt in with `/premium` and out with `/freemium` (must be logged in). Admins can enrol or remove players with `/authme premium ` / `/authme freemium `. +- **Direct-connection (offline-mode, no proxy):** requires [PacketEvents](https://github.com/retrooper/packetevents) 2.x. Without it, premium bypass is disabled at startup (fail-closed). +- **Behind an online-mode proxy (Velocity / BungeeCord):** the proxy authenticates with Mojang and forwards the verified UUID — no PacketEvents needed on the backend. Set `Hooks.bungeecord: true` on the backend. +- **Behind an offline-mode proxy:** install `authme-velocity` or `authme-bungee` on the proxy; premium players are authenticated per-player by the proxy and the verified UUID is forwarded to the backend. +- Full documentation: [docs/premium.md](docs/premium.md) #### Commands [Command list and usage](https://github.com/AuthMe/AuthMeReloaded/blob/master/docs/commands.md) @@ -145,7 +159,7 @@ AuthMe can display graphical login/register dialogs instead of chat-based prompt > - `AuthMe-*-Spigot-1.21.jar` (Spigot 1.20.x – 1.21.x) > - `AuthMe-*-Paper.jar` (Paper 1.21+) > - `AuthMe-*-Folia.jar` (Folia 1.21+) ->- PacketEvents (optional, required by some features) +>- [PacketEvents](https://github.com/retrooper/packetevents) 2.x (optional plugin; required for inventory protection, tab-complete blocking, and premium bypass) ## Credits diff --git a/authme-bungee/src/main/java/fr/xephi/authme/bungee/AuthMeBungeePlugin.java b/authme-bungee/src/main/java/fr/xephi/authme/bungee/AuthMeBungeePlugin.java index cda827aff..f63457c88 100644 --- a/authme-bungee/src/main/java/fr/xephi/authme/bungee/AuthMeBungeePlugin.java +++ b/authme-bungee/src/main/java/fr/xephi/authme/bungee/AuthMeBungeePlugin.java @@ -10,6 +10,7 @@ public void onEnable() { configManager = new BungeeConfigManager(getDataFolder().toPath()); BungeeAuthenticationStore authenticationStore = new BungeeAuthenticationStore(); proxyBridge = new BungeeProxyBridge(getProxy(), getLogger(), configManager.getConfiguration(), authenticationStore); + getProxy().getPluginManager().registerListener(this, proxyBridge); getProxy().getPluginManager().registerCommand(this, new BungeeReloadCommand(configManager, proxyBridge)); proxyBridge.logConfigurationDetails(); 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 02f958ec7..9392ccc83 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 @@ -10,8 +10,11 @@ import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.Server; import net.md_5.bungee.api.event.ChatEvent; +import net.md_5.bungee.api.event.LoginEvent; import net.md_5.bungee.api.event.PlayerDisconnectEvent; import net.md_5.bungee.api.event.PluginMessageEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.event.PreLoginEvent; import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.event.ServerSwitchEvent; import net.md_5.bungee.api.plugin.Listener; @@ -37,6 +40,10 @@ public final class BungeeProxyBridge implements Listener { private static final String PERFORM_LOGIN_MESSAGE = "perform.login"; private static final String PERFORM_LOGIN_ACK_MESSAGE = "perform.login.ack"; private static final String PROXY_STARTED_MESSAGE = "proxy.started"; + private static final String PREMIUM_SET_MESSAGE = "premium.set"; + private static final String PREMIUM_UNSET_MESSAGE = "premium.unset"; + private static final String PREMIUM_LIST_MESSAGE = "premium.list"; + private static final String PREMIUM_PENDING_SET_MESSAGE = "premium.pending.set"; private static final String PROXY_IDENTITY = "bungee"; private static final int MAX_RETRIES = 3; @@ -46,6 +53,11 @@ public final class BungeeProxyBridge implements Listener { private final BungeeAuthenticationStore authenticationStore; private final Map pendingAutoLogins = new ConcurrentHashMap<>(); private final Set notifiedAuthServers = ConcurrentHashMap.newKeySet(); + private volatile Set premiumUsernames = ConcurrentHashMap.newKeySet(); + // Players with a pending premium verification (ran /premium but not yet confirmed via reconnect) + private volatile Set pendingPremiumUsernames = ConcurrentHashMap.newKeySet(); + // Players whose Mojang UUID was confirmed by the proxy during the login phase (LoginSuccess with UUID v4) + private final Set proxyVerifiedPremium = ConcurrentHashMap.newKeySet(); private final ScheduledExecutorService retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "authme-bungee-retry"); t.setDaemon(true); @@ -60,6 +72,11 @@ public final class BungeeProxyBridge implements Listener { this.authenticationStore = authenticationStore; } + private void markProxyVerifiedPremium(String normalizedName) { + proxyVerifiedPremium.add(normalizedName); + logger.info("Proxy-verified premium: '" + normalizedName + "' authenticated online-mode with Mojang"); + } + void reload(BungeeProxyConfiguration configuration) { this.configuration = configuration; logger.info("Configuration reloaded"); @@ -145,6 +162,7 @@ public void onPluginMessage(PluginMessageEvent event) { + server.getInfo().getName() + "'"); authenticationStore.markAuthenticated(parsedMessage.playerName()); sendAutoLoginIfAlreadySwitched(parsedMessage.playerName(), server.getInfo()); + redirectToLoginServer(parsedMessage.playerName()); } else if (pendingAutoLogins.containsKey(parsedMessage.playerName())) { // Implicit ACK: login from non-auth server confirms perform.login was processed logger.info("Auto-login confirmed for " + parsedMessage.playerName() @@ -158,6 +176,28 @@ public void onPluginMessage(PluginMessageEvent event) { logger.info("Auto-login ACK received for " + parsedMessage.playerName() + " from server '" + server.getInfo().getName() + "'"); cancelPendingLogin(parsedMessage.playerName()); + } else if (PREMIUM_SET_MESSAGE.equals(parsedMessage.typeId())) { + premiumUsernames.add(parsedMessage.playerName()); + pendingPremiumUsernames.remove(parsedMessage.playerName()); + logger.fine(() -> "Premium enabled for '" + parsedMessage.playerName() + "' (proxy cache updated)"); + } else if (PREMIUM_UNSET_MESSAGE.equals(parsedMessage.typeId())) { + premiumUsernames.remove(parsedMessage.playerName()); + pendingPremiumUsernames.remove(parsedMessage.playerName()); + logger.fine(() -> "Premium disabled for '" + parsedMessage.playerName() + "' (proxy cache updated)"); + } else if (PREMIUM_PENDING_SET_MESSAGE.equals(parsedMessage.typeId())) { + pendingPremiumUsernames.add(parsedMessage.playerName()); + logger.fine(() -> "Pending premium verification started for '" + parsedMessage.playerName() + "'"); + } else if (PREMIUM_LIST_MESSAGE.equals(parsedMessage.typeId())) { + Set newPremiumSet = ConcurrentHashMap.newKeySet(); + if (!parsedMessage.playerName().isEmpty()) { + for (String name : parsedMessage.playerName().split(",")) { + if (!name.isEmpty()) { + newPremiumSet.add(name.trim()); + } + } + } + premiumUsernames = newPremiumSet; + logger.info("Premium list received from backend: " + premiumUsernames.size() + " premium player(s)"); } } @@ -173,7 +213,7 @@ public void onServerSwitch(ServerSwitchEvent event) { return; } - if (currentServer == null || !authenticationStore.isAuthenticated(player)) { + if (currentServer == null) { return; } @@ -184,6 +224,21 @@ public void onServerSwitch(ServerSwitchEvent event) { } String normalizedName = normalizeName(player.getName()); + + // Pending players have passed Mojang auth at the proxy, but we must NOT send PERFORM_LOGIN + // for them: the backend needs to run canBypassWithPremium() to finalize (persist) the premium + // UUID. Only confirmed premium players (premiumUsernames) trigger the auto-login bypass. + boolean isPremiumJoin = connectingToAuthServer + && proxyVerifiedPremium.contains(normalizedName) + && !pendingPremiumUsernames.contains(normalizedName); + if (!authenticationStore.isAuthenticated(player) && !isPremiumJoin) { + return; + } + if (isPremiumJoin) { + logger.fine("Proxy-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(AUTHME_CHANNEL, createPerformLoginMessage(normalizedName), false); @@ -254,6 +309,53 @@ public void onPlayerDisconnect(PlayerDisconnectEvent event) { } cancelPendingLogin(normalizedName); authenticationStore.clear(event.getPlayer()); + proxyVerifiedPremium.remove(normalizedName); + pendingPremiumUsernames.remove(normalizedName); + } + + @EventHandler + public void onPreLogin(PreLoginEvent event) { + String normalizedName = normalizeName(event.getConnection().getName()); + if (premiumUsernames.contains(normalizedName) || pendingPremiumUsernames.contains(normalizedName)) { + event.getConnection().setOnlineMode(true); + logger.fine("Forcing online-mode for premium player '" + normalizedName + "'"); + } + } + + /** + * Fires after the proxy has finished the Mojang authentication phase for a connecting player. + * If the connection ended up in online mode (real Mojang account verified at the proxy), the + * player is recorded as proxy-verified premium so the auto-login bypass on the auth server + * will fire on {@link ServerSwitchEvent}. + */ + @EventHandler + public void onLogin(LoginEvent event) { + if (event.isCancelled()) { + return; + } + if (!event.getConnection().isOnlineMode()) { + return; + } + String normalizedName = normalizeName(event.getConnection().getName()); + markProxyVerifiedPremium(normalizedName); + } + + /** + * Fallback: if for any reason the {@link LoginEvent} hook did not flag the player (e.g. the + * proxy is in global online mode and {@code isOnlineMode()} on PendingConnection is reported + * after {@code LoginEvent}), {@link PostLoginEvent} still gives us the verified UUID from the + * proxy. A version-4 UUID means Mojang verified the identity. + */ + @EventHandler + public void onPostLogin(PostLoginEvent event) { + ProxiedPlayer player = event.getPlayer(); + if (player.getUniqueId() != null && player.getUniqueId().version() == 4) { + String normalizedName = normalizeName(player.getName()); + if (proxyVerifiedPremium.add(normalizedName)) { + logger.info("Proxy-verified premium (PostLogin fallback): '" + normalizedName + + "' has a Mojang UUID"); + } + } } void shutdown() { @@ -350,16 +452,43 @@ private ParsedPluginMessage parsePluginMessage(byte[] data) { try { String typeId = input.readUTF(); if (!LOGIN_MESSAGE.equals(typeId) && !LOGOUT_MESSAGE.equals(typeId) - && !PERFORM_LOGIN_ACK_MESSAGE.equals(typeId)) { + && !PERFORM_LOGIN_ACK_MESSAGE.equals(typeId) + && !PREMIUM_SET_MESSAGE.equals(typeId) + && !PREMIUM_UNSET_MESSAGE.equals(typeId) + && !PREMIUM_LIST_MESSAGE.equals(typeId) + && !PREMIUM_PENDING_SET_MESSAGE.equals(typeId)) { return ParsedPluginMessage.ignored(); } - return new ParsedPluginMessage(typeId, normalizeName(input.readUTF())); + // premium.list carries a CSV in the second field, not a player name; read as-is + String argument = input.readUTF(); + return new ParsedPluginMessage(typeId, + PREMIUM_LIST_MESSAGE.equals(typeId) ? argument : normalizeName(argument)); } catch (IllegalStateException e) { logger.warning("Received malformed AuthMe plugin message on the authme:main channel"); return ParsedPluginMessage.ignored(); } } + private void redirectToLoginServer(String normalizedPlayerName) { + if (configuration.loginServer().isEmpty()) { + return; + } + ProxiedPlayer player = proxyServer.getPlayer(normalizedPlayerName); + if (player == null) { + logger.fine("Cannot redirect " + normalizedPlayerName + " to loginServer: player no longer on proxy"); + return; + } + ServerInfo targetServer = proxyServer.getServerInfo(configuration.loginServer()); + if (targetServer == null) { + logger.warning("loginServer '" + configuration.loginServer() + + "' is not registered on the proxy; cannot redirect " + normalizedPlayerName); + return; + } + logger.info("Redirecting " + normalizedPlayerName + " to login server '" + + configuration.loginServer() + "' after authentication"); + player.connect(targetServer); + } + private void redirectLoggedOutPlayer(String normalizedPlayerName) { if (!configuration.sendOnLogoutEnabled()) { return; diff --git a/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyConfiguration.java b/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyConfiguration.java index 42f1d4f20..0ff9677cf 100644 --- a/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyConfiguration.java +++ b/authme-bungee/src/main/java/fr/xephi/authme/bungee/BungeeProxyConfiguration.java @@ -21,6 +21,7 @@ final class BungeeProxyConfiguration { private final boolean autoLoginEnabled; private final boolean sendOnLogoutEnabled; private final String sendOnLogoutTarget; + private final String loginServer; private final String sharedSecret; BungeeProxyConfiguration(Set authServers, boolean allServersAreAuthServers, @@ -28,7 +29,7 @@ final class BungeeProxyConfiguration { boolean chatRequiresAuth, boolean serverSwitchRequiresAuth, String serverSwitchKickMessage, boolean autoLoginEnabled, boolean sendOnLogoutEnabled, String sendOnLogoutTarget, - String sharedSecret) { + String loginServer, String sharedSecret) { this.authServers = authServers; this.allServersAreAuthServers = allServersAreAuthServers; this.commandsRequireAuth = commandsRequireAuth; @@ -39,6 +40,7 @@ final class BungeeProxyConfiguration { this.autoLoginEnabled = autoLoginEnabled; this.sendOnLogoutEnabled = sendOnLogoutEnabled; this.sendOnLogoutTarget = normalizeServerName(sendOnLogoutTarget); + this.loginServer = normalizeServerName(loginServer); this.sharedSecret = sharedSecret; } @@ -54,6 +56,7 @@ static BungeeProxyConfiguration from(SettingsManager settingsManager) { settingsManager.getProperty(BungeeConfigProperties.AUTOLOGIN), settingsManager.getProperty(BungeeConfigProperties.ENABLE_SEND_ON_LOGOUT), settingsManager.getProperty(BungeeConfigProperties.SEND_ON_LOGOUT_TARGET), + settingsManager.getProperty(BungeeConfigProperties.LOGIN_SERVER), settingsManager.getProperty(BungeeConfigProperties.PROXY_SHARED_SECRET)); } @@ -93,6 +96,10 @@ String sendOnLogoutTarget() { return sendOnLogoutTarget; } + String loginServer() { + return loginServer; + } + String sharedSecret() { return sharedSecret; } diff --git a/authme-bungee/src/main/java/fr/xephi/authme/bungee/config/BungeeConfigProperties.java b/authme-bungee/src/main/java/fr/xephi/authme/bungee/config/BungeeConfigProperties.java index 831de3716..9c7f23c37 100644 --- a/authme-bungee/src/main/java/fr/xephi/authme/bungee/config/BungeeConfigProperties.java +++ b/authme-bungee/src/main/java/fr/xephi/authme/bungee/config/BungeeConfigProperties.java @@ -52,6 +52,13 @@ public final class BungeeConfigProperties implements SettingsHolder { public static final Property SEND_ON_LOGOUT_TARGET = newProperty("unloggedUserServer", ""); + @Comment({ + "Server to redirect players to after successful authentication on an auth server.", + "Leave empty to disable proxy-side login redirect (backend handles it via BUNGEECORD_SERVER)." + }) + public static final Property LOGIN_SERVER = + newProperty("loginServer", ""); + @Comment({ "Shared secret used to sign perform.login messages sent to backend servers.", "Generated automatically on first start — copy this value to the Hooks.proxySharedSecret", 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 de0549ae5..a94df1b1f 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 @@ -155,7 +155,7 @@ void shouldRedirectPlayerOnLogoutWhenConfigured() { BungeeProxyBridge bridge = new BungeeProxyBridge( proxyServer, logger, new BungeeProxyConfiguration( Set.of("lobby"), false, true, Set.of("/login"), true, true, - "Authentication required.", true, true, "limbo", ""), + "Authentication required.", true, true, "limbo", "", ""), new BungeeAuthenticationStore()); bridge.onPluginMessage(pluginMessageEvent); @@ -441,7 +441,7 @@ void shouldSendAutoLoginImmediatelyWhenPlayerAlreadySwitchedBeforeLoginMessage() private static BungeeProxyConfiguration createConfiguration() { return new BungeeProxyConfiguration( Set.of("lobby"), false, true, Set.of("/login", "/register", "/l", "/reg", "/email", "/captcha", "/2fa", "/totp", "/log"), - true, true, "Authentication required.", true, false, "", "test-secret"); + true, true, "Authentication required.", true, false, "", "", "test-secret"); } private static byte[] createAuthMePayload(String typeId, String playerName) { diff --git a/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeReloadCommandTest.java b/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeReloadCommandTest.java index 6b45ba6e7..6f06f8d2e 100644 --- a/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeReloadCommandTest.java +++ b/authme-bungee/src/test/java/fr/xephi/authme/bungee/BungeeReloadCommandTest.java @@ -28,7 +28,7 @@ class BungeeReloadCommandTest { void shouldReloadConfigAndProxyBridge() { BungeeProxyConfiguration configuration = new BungeeProxyConfiguration( Set.of("lobby"), false, true, Set.of("/login"), true, true, - "Authentication required.", true, false, "", ""); + "Authentication required.", true, false, "", "", ""); given(configManager.reload()).willReturn(configuration); BungeeReloadCommand command = new BungeeReloadCommand(configManager, proxyBridge); diff --git a/authme-core/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/authme-core/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 51c88f36b..e932980ac 100644 --- a/authme-core/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/authme-core/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -49,6 +49,10 @@ import fr.xephi.authme.command.executable.totp.TotpCodeCommand; import fr.xephi.authme.command.executable.unregister.UnregisterCommand; import fr.xephi.authme.command.executable.verification.VerificationCommand; +import fr.xephi.authme.command.executable.authme.SetFreemiumAdminCommand; +import fr.xephi.authme.command.executable.authme.SetPremiumAdminCommand; +import fr.xephi.authme.command.executable.premium.PremiumCommand; +import fr.xephi.authme.command.executable.premium.FreemiumCommand; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.DebugSectionPermissions; import fr.xephi.authme.permission.PlayerPermission; @@ -169,8 +173,29 @@ private void buildCommands() { .executableCommand(VerificationCommand.class) .register(); + // Register the /premium command + CommandDescription premiumBase = CommandDescription.builder() + .parent(null) + .labels("premium") + .description("Enable premium mode") + .detailedDescription("Enables premium mode: skip authentication with a verified Mojang account.") + .permission(PlayerPermission.USE_PREMIUM) + .executableCommand(PremiumCommand.class) + .register(); + + // Register the /freemium command + CommandDescription freemiumBase = CommandDescription.builder() + .parent(null) + .labels("freemium") + .description("Disable premium mode") + .detailedDescription("Disables premium mode and restores password-based authentication.") + .permission(PlayerPermission.USE_FREEMIUM) + .executableCommand(FreemiumCommand.class) + .register(); + List baseCommands = ImmutableList.of(authMeBase, emailBase, loginBase, logoutBase, - registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase); + registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase, + premiumBase, freemiumBase); setHelpOnAllBases(baseCommands); commands = baseCommands; @@ -468,6 +493,26 @@ private CommandDescription buildAuthMeBaseCommand() { .executableCommand(RecentPlayersCommand.class) .register(); + CommandDescription.builder() + .parent(authmeBase) + .labels("premium", "setpremium") + .description("Enable premium for a player") + .detailedDescription("Enables premium mode for the specified player.") + .withArgument("player", "Player name", MANDATORY) + .permission(AdminPermission.SET_PREMIUM) + .executableCommand(SetPremiumAdminCommand.class) + .register(); + + CommandDescription.builder() + .parent(authmeBase) + .labels("freemium", "setfreemium") + .description("Disable premium for a player") + .detailedDescription("Disables premium mode for the specified player.") + .withArgument("player", "Player name", MANDATORY) + .permission(AdminPermission.SET_FREEMIUM) + .executableCommand(SetFreemiumAdminCommand.class) + .register(); + CommandDescription.builder() .parent(authmeBase) .labels("debug", "dbg") diff --git a/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetFreemiumAdminCommand.java b/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetFreemiumAdminCommand.java new file mode 100644 index 000000000..af882f0bc --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetFreemiumAdminCommand.java @@ -0,0 +1,22 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.service.PremiumService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Admin command to disable premium mode for a player by name. + */ +public class SetFreemiumAdminCommand implements ExecutableCommand { + + @Inject + private PremiumService premiumService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + premiumService.disablePremiumAdmin(sender, arguments.get(0)); + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetPremiumAdminCommand.java b/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetPremiumAdminCommand.java new file mode 100644 index 000000000..ba036cd3a --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/command/executable/authme/SetPremiumAdminCommand.java @@ -0,0 +1,22 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.service.PremiumService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Admin command to enable premium mode for a player by name. + */ +public class SetPremiumAdminCommand implements ExecutableCommand { + + @Inject + private PremiumService premiumService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + premiumService.enablePremiumAdmin(sender, arguments.get(0)); + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/command/executable/premium/FreemiumCommand.java b/authme-core/src/main/java/fr/xephi/authme/command/executable/premium/FreemiumCommand.java new file mode 100644 index 000000000..431fd4f23 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/command/executable/premium/FreemiumCommand.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.command.executable.premium; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.PremiumService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to disable premium mode for a player. + */ +public class FreemiumCommand extends PlayerCommand { + + @Inject + private PremiumService premiumService; + + @Override + protected void runCommand(Player player, List arguments) { + premiumService.disablePremium(player); + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.UNKNOWN_COMMAND; + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/command/executable/premium/PremiumCommand.java b/authme-core/src/main/java/fr/xephi/authme/command/executable/premium/PremiumCommand.java new file mode 100644 index 000000000..a1ee67327 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/command/executable/premium/PremiumCommand.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.command.executable.premium; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.PremiumService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to enable premium mode for a player with an official Minecraft account. + */ +public class PremiumCommand extends PlayerCommand { + + @Inject + private PremiumService premiumService; + + @Override + protected void runCommand(Player player, List arguments) { + premiumService.enablePremium(player); + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.UNKNOWN_COMMAND; + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/authme-core/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java index 3d78090e7..58e3ccfb9 100644 --- a/authme-core/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java +++ b/authme-core/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -44,6 +44,7 @@ public class PlayerAuth { private float yaw; private float pitch; private UUID uuid; + private UUID premiumUuid; /** * Hidden constructor. @@ -183,6 +184,18 @@ public void setUuid(UUID uuid) { this.uuid = uuid; } + public UUID getPremiumUuid() { + return premiumUuid; + } + + public void setPremiumUuid(UUID premiumUuid) { + this.premiumUuid = premiumUuid; + } + + public boolean isPremium() { + return premiumUuid != null; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof PlayerAuth)) { @@ -234,6 +247,7 @@ public static final class Builder { private float yaw; private float pitch; private UUID uuid; + private UUID premiumUuid; /** * Creates a PlayerAuth object. @@ -260,6 +274,7 @@ public PlayerAuth build() { auth.yaw = yaw; auth.pitch = pitch; auth.uuid = uuid; + auth.premiumUuid = premiumUuid; return auth; } @@ -371,5 +386,10 @@ public Builder uuid(UUID uuid) { this.uuid = uuid; return this; } + + public Builder premiumUuid(UUID premiumUuid) { + this.premiumUuid = premiumUuid; + return this; + } } } diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java b/authme-core/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java index 6c12a1710..081e261ae 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java @@ -16,6 +16,7 @@ import static ch.jalu.datasourcecolumns.data.UpdateValues.with; import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.isNotNull; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; /** @@ -153,6 +154,21 @@ public int getAccountsRegistered() { return columnsHandler.count(new AlwaysTruePredicate<>()); } + @Override + public boolean updatePremiumUuid(PlayerAuth auth) { + return columnsHandler.update(auth, AuthMeColumns.PREMIUM_UUID); + } + + @Override + public List getPremiumUsernames() { + try { + return columnsHandler.retrieve(isNotNull(AuthMeColumns.PREMIUM_UUID), AuthMeColumns.NAME); + } catch (SQLException e) { + logSqlException(e); + return Collections.emptyList(); + } + } + @Override public boolean updateRealName(String user, String realName) { return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/authme-core/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index f6030e920..309cc19f9 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -183,6 +183,20 @@ public boolean updateEmail(final PlayerAuth auth) { return result; } + @Override + public boolean updatePremiumUuid(PlayerAuth auth) { + boolean result = source.updatePremiumUuid(auth); + if (result) { + cachedAuths.put(auth.getNickname(), Optional.of(auth)); + } + return result; + } + + @Override + public List getPremiumUsernames() { + return source.getPremiumUsernames(); + } + @Override public List getAllAuthsByIp(String ip) { return source.getAllAuthsByIp(ip); diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/Columns.java b/authme-core/src/main/java/fr/xephi/authme/datasource/Columns.java index a604d0a4d..148d51aab 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -31,6 +31,7 @@ public final class Columns { public final String REGISTRATION_DATE; public final String REGISTRATION_IP; public final String PLAYER_UUID; + public final String PREMIUM_UUID; public Columns(Settings settings) { NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); @@ -54,6 +55,7 @@ public Columns(Settings settings) { REGISTRATION_DATE = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_DATE); REGISTRATION_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_IP); PLAYER_UUID = settings.getProperty(DatabaseSettings.MYSQL_COL_PLAYER_UUID); + PREMIUM_UUID = settings.getProperty(DatabaseSettings.MYSQL_COL_PREMIUM_UUID); } } diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/DataSource.java b/authme-core/src/main/java/fr/xephi/authme/datasource/DataSource.java index 3152eb17e..75446f51e 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -261,6 +261,23 @@ default boolean removeTotpKey(String user) { return setTotpKey(user, null); } + /** + * Updates the premium UUID of the given PlayerAuth object. + * Set {@link PlayerAuth#getPremiumUuid()} to the Mojang UUID to enable premium mode, + * or to null to disable it. + * + * @param auth the PlayerAuth whose premium UUID should be updated + * @return True upon success, false upon failure + */ + boolean updatePremiumUuid(PlayerAuth auth); + + /** + * Returns a list of all lowercase player names that have premium mode enabled. + * + * @return list of premium player names (lowercase) + */ + List getPremiumUsernames(); + /** * Reload the data source. */ diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/MySQL.java b/authme-core/src/main/java/fr/xephi/authme/datasource/MySQL.java index 341c2bcaf..6a7ea93be 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -293,6 +293,11 @@ private void checkTablesAndColumns() throws SQLException { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); } + + if (!col.PREMIUM_UUID.isEmpty() && isColumnMissing(md, col.PREMIUM_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PREMIUM_UUID + " VARCHAR(36)"); + } } logger.info("MySQL setup finished"); } @@ -490,6 +495,8 @@ private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); UUID uuid = col.PLAYER_UUID.isEmpty() ? null : UuidUtils.parseUuidSafely(row.getString(col.PLAYER_UUID)); + UUID premiumUuid = col.PREMIUM_UUID.isEmpty() + ? null : UuidUtils.parseUuidSafely(row.getString(col.PREMIUM_UUID)); return PlayerAuth.builder() .name(row.getString(col.NAME)) .realName(row.getString(col.REAL_NAME)) @@ -508,6 +515,7 @@ private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { .locYaw(row.getFloat(col.LASTLOC_YAW)) .locPitch(row.getFloat(col.LASTLOC_PITCH)) .uuid(uuid) + .premiumUuid(premiumUuid) .build(); } } diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java b/authme-core/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java index cc308934b..3b7dce8c7 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java @@ -12,6 +12,7 @@ import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.util.UuidUtils; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -25,6 +26,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.UUID; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -253,6 +255,11 @@ private void checkTablesAndColumns() throws SQLException { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); } + + if (!col.PREMIUM_UUID.isEmpty() && isColumnMissing(md, col.PREMIUM_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PREMIUM_UUID + " VARCHAR(36)"); + } } logger.info("PostgreSQL setup finished"); } @@ -450,6 +457,8 @@ public boolean setTotpKey(String user, String totpKey) { private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); + UUID premiumUuid = col.PREMIUM_UUID.isEmpty() + ? null : UuidUtils.parseUuidSafely(row.getString(col.PREMIUM_UUID)); return PlayerAuth.builder() .name(row.getString(col.NAME)) .realName(row.getString(col.REAL_NAME)) @@ -467,6 +476,7 @@ private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { .locZ(row.getDouble(col.LASTLOC_Z)) .locYaw(row.getFloat(col.LASTLOC_YAW)) .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .premiumUuid(premiumUuid) .build(); } } diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/SQLite.java b/authme-core/src/main/java/fr/xephi/authme/datasource/SQLite.java index ca201a1ff..be4487729 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -25,6 +25,10 @@ import java.util.Locale; import java.util.Set; +import fr.xephi.authme.util.UuidUtils; + +import java.util.UUID; + import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -193,6 +197,11 @@ protected void setup() throws SQLException { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); } + + if (!col.PREMIUM_UUID.isEmpty() && isColumnMissing(md, col.PREMIUM_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PREMIUM_UUID + " VARCHAR(36)"); + } } logger.info("SQLite Setup finished"); } @@ -369,6 +378,8 @@ public synchronized boolean setTotpKey(String user, String totpKey) { private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; + UUID premiumUuid = col.PREMIUM_UUID.isEmpty() + ? null : UuidUtils.parseUuidSafely(row.getString(col.PREMIUM_UUID)); return PlayerAuth.builder() .name(row.getString(col.NAME)) @@ -386,6 +397,7 @@ private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { .locWorld(row.getString(col.LASTLOC_WORLD)) .locYaw(row.getFloat(col.LASTLOC_YAW)) .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .premiumUuid(premiumUuid) .build(); } diff --git a/authme-core/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java b/authme-core/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java index 7c4d2e6a7..eed42f53d 100644 --- a/authme-core/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java +++ b/authme-core/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java @@ -53,6 +53,11 @@ public final class AuthMeColumns { auth -> ( auth.getUuid() == null ? null : auth.getUuid().toString()), OPTIONAL); + public static final PlayerAuthColumn PREMIUM_UUID = createString( + DatabaseSettings.MYSQL_COL_PREMIUM_UUID, + auth -> auth.getPremiumUuid() == null ? null : auth.getPremiumUuid().toString(), + OPTIONAL); + // -------- // Location columns // -------- diff --git a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Decoder.java b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Decoder.java new file mode 100644 index 000000000..37f8c4c97 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Decoder.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.listener.packetevents; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.buffer.ByteBuf; + +import javax.crypto.Cipher; +import java.util.List; + +/** + * Netty inbound handler that AES/CFB8-decrypts the raw byte stream from the client. + * Must be inserted BEFORE the frame-splitter so the splitter sees plaintext frames. + */ +class AesCfb8Decoder extends MessageToMessageDecoder { + + private final Cipher cipher; + + AesCfb8Decoder(Cipher cipher) { + this.cipher = cipher; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) { + byte[] input = new byte[msg.readableBytes()]; + msg.readBytes(input); + out.add(Unpooled.wrappedBuffer(cipher.update(input))); + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Encoder.java b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Encoder.java new file mode 100644 index 000000000..8b86947fe --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/AesCfb8Encoder.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.listener.packetevents; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import javax.crypto.Cipher; + +/** + * Netty outbound handler that AES/CFB8-encrypts the byte stream to the client. + * Must be inserted BEFORE the length-prepender so the prepended length is part of the + * encrypted stream (matching Minecraft's protocol: the length varint is also encrypted). + */ +class AesCfb8Encoder extends MessageToByteEncoder { + + private final Cipher cipher; + + AesCfb8Encoder(Cipher cipher) { + this.cipher = cipher; + } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) { + byte[] input = new byte[in.readableBytes()]; + in.readBytes(input); + out.writeBytes(cipher.update(input)); + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsListenerRegistry.java b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsListenerRegistry.java index d50857262..294b1ba8d 100644 --- a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsListenerRegistry.java +++ b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsListenerRegistry.java @@ -4,6 +4,8 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.platform.PacketInterceptionAdapter; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumLoginVerifier; import org.bukkit.entity.Player; /** @@ -14,6 +16,7 @@ public final class PacketEventsListenerRegistry implements PacketInterceptionAda private InventoryPacketListener inventoryPacketListener; private TabCompletePacketListener tabCompletePacketListener; + private PremiumVerificationPacketListener premiumVerificationPacketListener; @Override public void registerInventoryProtection(PlayerCache playerCache, DataSource dataSource) { @@ -53,4 +56,22 @@ public void unregisterTabCompleteBlock() { tabCompletePacketListener = null; } } + + @Override + public void registerPremiumVerification(DataSource dataSource, PremiumLoginVerifier verifier, + PendingPremiumCache pendingPremiumCache) { + if (premiumVerificationPacketListener == null) { + premiumVerificationPacketListener = + new PremiumVerificationPacketListener(dataSource, verifier, pendingPremiumCache); + } + PacketEvents.getAPI().getEventManager().registerListener(premiumVerificationPacketListener); + } + + @Override + public void unregisterPremiumVerification() { + if (premiumVerificationPacketListener != null) { + PacketEvents.getAPI().getEventManager().unregisterListener(premiumVerificationPacketListener); + premiumVerificationPacketListener = null; + } + } } diff --git a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsService.java b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsService.java index adb9b15b4..6ccac57cc 100644 --- a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsService.java +++ b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PacketEventsService.java @@ -8,7 +8,11 @@ import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.platform.PacketInterceptionAdapter; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumLoginVerifier; import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.PremiumSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -22,21 +26,29 @@ public class PacketEventsService implements SettingsDependent { private boolean protectInvBeforeLogin; private boolean denyTabCompleteBeforeLogin; + private boolean enablePremium; + private boolean bungeecordEnabled; private boolean inventoryProtectionRegistered; private boolean tabCompleteBlockRegistered; + private boolean premiumVerificationRegistered; private final BukkitService bukkitService; private final PlayerCache playerCache; private final DataSource dataSource; private final PacketInterceptionAdapter packetInterceptionAdapter; + private final PremiumLoginVerifier premiumLoginVerifier; + private final PendingPremiumCache pendingPremiumCache; @Inject PacketEventsService(Settings settings, BukkitService bukkitService, PlayerCache playerCache, - DataSource dataSource, PacketInterceptionAdapter packetInterceptionAdapter) { + DataSource dataSource, PacketInterceptionAdapter packetInterceptionAdapter, + PremiumLoginVerifier premiumLoginVerifier, PendingPremiumCache pendingPremiumCache) { this.bukkitService = bukkitService; this.playerCache = playerCache; this.dataSource = dataSource; this.packetInterceptionAdapter = packetInterceptionAdapter; + this.premiumLoginVerifier = premiumLoginVerifier; + this.pendingPremiumCache = pendingPremiumCache; reload(settings); } @@ -44,13 +56,23 @@ public class PacketEventsService implements SettingsDependent { * Sets up the PacketEvents packet listeners. */ public void setup() { - if (!Bukkit.getPluginManager().isPluginEnabled("packetevents")) { + boolean isProxyMode = bungeecordEnabled || packetInterceptionAdapter.isProxyForwardingEnabled(); + boolean needsPremiumPacketVerification = enablePremium + && !Bukkit.getServer().getOnlineMode() + && !isProxyMode; + + boolean packetEventsAvailable = Bukkit.getPluginManager().isPluginEnabled("packetevents"); + if (!packetEventsAvailable) { if (protectInvBeforeLogin) { logger.warning("WARNING! The protectInventory feature requires PacketEvents! Disabling it..."); } if (denyTabCompleteBeforeLogin) { logger.warning("WARNING! The denyTabComplete feature requires PacketEvents! Disabling it..."); } + if (needsPremiumPacketVerification) { + logger.warning("WARNING! Premium bypass requires the PacketEvents plugin for session " + + "verification! Premium auto-login is disabled until PacketEvents is installed."); + } return; } @@ -80,6 +102,17 @@ public void setup() { packetInterceptionAdapter.unregisterTabCompleteBlock(); tabCompleteBlockRegistered = false; } + + if (needsPremiumPacketVerification) { + if (!premiumVerificationRegistered) { + packetInterceptionAdapter.registerPremiumVerification(dataSource, premiumLoginVerifier, + pendingPremiumCache); + premiumVerificationRegistered = true; + } + } else if (premiumVerificationRegistered) { + packetInterceptionAdapter.unregisterPremiumVerification(); + premiumVerificationRegistered = false; + } } /** @@ -94,6 +127,10 @@ public void disable() { packetInterceptionAdapter.unregisterTabCompleteBlock(); tabCompleteBlockRegistered = false; } + if (premiumVerificationRegistered) { + packetInterceptionAdapter.unregisterPremiumVerification(); + premiumVerificationRegistered = false; + } } /** @@ -113,6 +150,8 @@ public void reload(Settings settings) { this.protectInvBeforeLogin = settings.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); this.denyTabCompleteBeforeLogin = settings.getProperty(RestrictionSettings.DENY_TABCOMPLETE_BEFORE_LOGIN); + this.enablePremium = settings.getProperty(PremiumSettings.ENABLE_PREMIUM); + this.bungeecordEnabled = settings.getProperty(HooksSettings.BUNGEECORD); // If inventory protection was on and is now disabled, restore inventories for online players if (oldProtectInventory && !protectInvBeforeLogin && inventoryProtectionRegistered) { diff --git a/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PremiumVerificationPacketListener.java b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PremiumVerificationPacketListener.java new file mode 100644 index 000000000..40c560769 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/listener/packetevents/PremiumVerificationPacketListener.java @@ -0,0 +1,222 @@ +package fr.xephi.authme.listener.packetevents; + +import com.github.retrooper.packetevents.event.PacketListenerAbstract; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.netty.channel.ChannelHelper; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.login.client.WrapperLoginClientEncryptionResponse; +import com.github.retrooper.packetevents.wrapper.login.client.WrapperLoginClientLoginStart; +import com.github.retrooper.packetevents.wrapper.login.server.WrapperLoginServerEncryptionRequest; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumLoginVerifier; +import io.netty.channel.ChannelPipeline; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.net.InetSocketAddress; +import java.security.GeneralSecurityException; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +/** + * Intercepts {@code LOGIN_START} and {@code ENCRYPTION_RESPONSE} packets to perform + * a cryptographic premium identity check against Mojang's session server. + * + *

    For premium players:

    + *
      + *
    1. Cancels {@code LOGIN_START} and sends a synthetic {@code EncryptionRequest}.
    2. + *
    3. Cancels {@code ENCRYPTION_RESPONSE}; decrypts and verifies with Mojang + * ({@code hasJoined}) on an async thread.
    4. + *
    5. Stores the Mojang UUID in {@link PremiumLoginVerifier} on success.
    6. + *
    7. Re-injects a {@code LOGIN_START} so vanilla processes the login normally.
    8. + *
    + * + *

    For non-premium players, all packets pass through unmodified.

    + */ +public class PremiumVerificationPacketListener extends PacketListenerAbstract { + + private static final Pattern VALID_USERNAME = Pattern.compile("^[a-zA-Z0-9_]{2,16}$"); + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PremiumVerificationPacketListener.class); + + private final DataSource dataSource; + private final PremiumLoginVerifier loginVerifier; + private final PendingPremiumCache pendingPremiumCache; + + public PremiumVerificationPacketListener(DataSource dataSource, PremiumLoginVerifier loginVerifier, + PendingPremiumCache pendingPremiumCache) { + this.dataSource = dataSource; + this.loginVerifier = loginVerifier; + this.pendingPremiumCache = pendingPremiumCache; + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + if (event.getPacketType() == PacketType.Login.Client.LOGIN_START) { + handleLoginStart(event); + } else if (event.getPacketType() == PacketType.Login.Client.ENCRYPTION_RESPONSE) { + handleEncryptionResponse(event); + } + } + + private void handleLoginStart(PacketReceiveEvent event) { + WrapperLoginClientLoginStart wrapper = new WrapperLoginClientLoginStart(event); + String username = wrapper.getUsername(); + if (username == null || !VALID_USERNAME.matcher(username).matches()) { + return; + } + + // Capture state before the event is recycled + User user = event.getUser(); + ClientVersion clientVersion = user.getClientVersion(); + String connectionKey = connectionKey(user); + // MC >= 1.20.2 includes the player UUID in LOGIN_START; must be forwarded when re-injecting + UUID playerUUID = wrapper.getPlayerUUID().orElse(null); + + // Cancel immediately so the vanilla server does not process this LOGIN_START yet + event.setCancelled(true); + + // DB lookup must happen off the Netty event loop to avoid blocking + CompletableFuture.runAsync(() -> { + String lowerName = username.toLowerCase(Locale.ROOT); + fr.xephi.authme.data.auth.PlayerAuth auth = dataSource.getAuth(lowerName); + + boolean isPremium = auth != null && auth.isPremium(); + boolean isPending = !isPremium && pendingPremiumCache.isPending(username); + + if (isPremium || isPending) { + byte[] verifyToken = loginVerifier.startVerification(connectionKey, username, playerUUID); + WrapperLoginServerEncryptionRequest encReq = new WrapperLoginServerEncryptionRequest( + "", loginVerifier.getPublicKey(), verifyToken, true); + user.sendPacket(encReq); + } else { + // Not premium (and not pending) — resume normal login without verification + resumeLogin(user, username, clientVersion, playerUUID); + } + }); + } + + private void handleEncryptionResponse(PacketReceiveEvent event) { + User user = event.getUser(); + String connectionKey = connectionKey(user); + + if (!loginVerifier.hasPending(connectionKey)) { + return; + } + + String username = loginVerifier.getPendingUsername(connectionKey); + // Read playerUUID BEFORE completeVerification() removes the pending record + UUID playerUUID = loginVerifier.getPendingPlayerUUID(connectionKey); + ClientVersion clientVersion = user.getClientVersion(); + + WrapperLoginClientEncryptionResponse wrapper = new WrapperLoginClientEncryptionResponse(event); + + Optional encVerifyTokenOpt = wrapper.getEncryptedVerifyToken(); + if (!encVerifyTokenOpt.isPresent()) { + // Client responded with a signed nonce (1.19.1+ chat signing). We sent a plain verify + // token so this should not happen; fall through to password auth without verifying. + logger.warning("Player '" + username + "' returned a signed nonce during premium " + + "verification (expected plain verify token). Resuming without premium bypass."); + loginVerifier.cleanupPending(connectionKey); + event.setCancelled(true); + resumeLogin(user, username, clientVersion, playerUUID); + return; + } + + byte[] encSharedSecret = wrapper.getEncryptedSharedSecret().clone(); + byte[] encVerifyToken = encVerifyTokenOpt.get().clone(); + event.setCancelled(true); + + // Step 1: RSA-decrypt the shared secret synchronously (fast; we're on the event loop). + // We must do this here — not inside the async task — so we can set up Netty encryption + // immediately before returning. The client is already in AES-encrypted mode after + // sending ENCRYPTION_RESPONSE; without decryption on our side every subsequent packet + // arrives as garbled bytes and Netty throws "length wider than 21-bit". + byte[] sharedSecret; + try { + sharedSecret = loginVerifier.decryptData(encSharedSecret); + } catch (GeneralSecurityException e) { + logger.warning("RSA decryption failed for '" + username + "': " + e.getMessage()); + loginVerifier.cleanupPending(connectionKey); + resumeLogin(user, username, clientVersion, playerUUID); + return; + } + + // Step 2: Install AES cipher handlers in the Netty pipeline synchronously. + enableChannelEncryption(user.getChannel(), sharedSecret); + + // Step 3: Async — verify the token and call Mojang's hasJoined endpoint. + loginVerifier.completeVerification(connectionKey, sharedSecret, encVerifyToken) + .thenAccept(maybeUuid -> { + maybeUuid.ifPresent(uuid -> loginVerifier.storeVerified(username, uuid)); + resumeLogin(user, username, clientVersion, playerUUID); + }) + .exceptionally(ex -> { + logger.warning("Unexpected error during premium verification for '" + + username + "': " + ex.getMessage()); + resumeLogin(user, username, clientVersion, playerUUID); + return null; + }); + } + + /** + * Installs AES/CFB8 cipher handlers into the Netty pipeline. + * + *

    Must be called on the channel's event-loop thread (i.e., synchronously from a + * PacketEvents {@code onPacketReceive} handler). After the client sends + * {@code ENCRYPTION_RESPONSE} it immediately encrypts all outbound traffic, so + * decryption must be in place before any subsequent packet arrives.

    + * + *

    Handler positions follow vanilla Minecraft's {@code Connection.setupEncryption()}: + * {@code decrypt} before {@code "splitter"} (decrypt raw bytes before frame-splitting), + * {@code encrypt} before {@code "prepender"} (encrypt after length-prefixing, since the + * length varint is inside the encrypted stream in the Minecraft protocol).

    + */ + private void enableChannelEncryption(Object channel, byte[] sharedSecret) { + try { + SecretKeySpec key = new SecretKeySpec(sharedSecret, "AES"); + // In Minecraft, the IV equals the shared secret (same 16 bytes for key and IV) + IvParameterSpec iv = new IvParameterSpec(sharedSecret); + + Cipher decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding"); + decryptCipher.init(Cipher.DECRYPT_MODE, key, iv); + + Cipher encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding"); + encryptCipher.init(Cipher.ENCRYPT_MODE, key, iv); + + ChannelPipeline pipeline = (ChannelPipeline) ChannelHelper.getPipeline(channel); + pipeline.addBefore("splitter", "decrypt", new AesCfb8Decoder(decryptCipher)); + pipeline.addBefore("prepender", "encrypt", new AesCfb8Encoder(encryptCipher)); + } catch (GeneralSecurityException e) { + logger.warning("Failed to install AES cipher handlers: " + e.getMessage()); + } + } + + /** + * Re-injects a {@code LOGIN_START} packet so vanilla completes the login normally. + * Uses {@code receivePacketSilently} to bypass PacketEvents' own listener chain + * (prevents infinite interception of our own injected packet). + * + *

    The {@code playerUUID} must be forwarded for MC >= 1.20.2 clients; it is {@code null} + * on older protocol versions.

    + */ + private void resumeLogin(User user, String username, ClientVersion clientVersion, UUID playerUUID) { + WrapperLoginClientLoginStart resumePacket = + new WrapperLoginClientLoginStart(clientVersion, username, null, playerUUID); + user.receivePacketSilently(resumePacket); + } + + private static String connectionKey(User user) { + InetSocketAddress addr = user.getAddress(); + return addr.getAddress().getHostAddress() + ":" + addr.getPort(); + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/message/MessageKey.java b/authme-core/src/main/java/fr/xephi/authme/message/MessageKey.java index 88d576751..8b82e8e2b 100644 --- a/authme-core/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/authme-core/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -450,7 +450,61 @@ public enum MessageKey { SPAWN_NOT_DEFINED("admin.spawn.not_defined"), /** First spawn has failed, please try to define the first spawn. */ - FIRST_SPAWN_NOT_DEFINED("admin.spawn.first_not_defined"); + FIRST_SPAWN_NOT_DEFINED("admin.spawn.first_not_defined"), + + /** Premium mode is not enabled on this server. */ + PREMIUM_FEATURE_DISABLED("premium.feature_disabled"), + + /** No premium Minecraft account found for your username. */ + PREMIUM_ACCOUNT_NOT_FOUND("premium.account_not_found"), + + /** Premium mode is already enabled for your account. */ + PREMIUM_ALREADY_ENABLED("premium.already_enabled"), + + /** Premium mode enabled! You will no longer need to authenticate. */ + PREMIUM_ENABLE_SUCCESS("premium.enable_success"), + + /** Premium mode is not enabled for your account. */ + PREMIUM_NOT_ENABLED("premium.not_enabled"), + + /** Premium mode disabled. You will need to authenticate again. */ + PREMIUM_DISABLE_SUCCESS("premium.disable_success"), + + /** An error occurred while verifying your premium status. */ + PREMIUM_ERROR("premium.error"), + + /** %name is not registered. */ + PREMIUM_ADMIN_NOT_REGISTERED("premium.admin.not_registered", "%name"), + + /** Premium mode is already enabled for %name. */ + PREMIUM_ADMIN_ALREADY_ENABLED("premium.admin.already_enabled", "%name"), + + /** No Mojang account found for %name. */ + PREMIUM_ADMIN_ACCOUNT_NOT_FOUND("premium.admin.account_not_found", "%name"), + + /** Premium mode enabled for %name. */ + PREMIUM_ADMIN_ENABLE_SUCCESS("premium.admin.enable_success", "%name"), + + /** Premium mode is not enabled for %name. */ + PREMIUM_ADMIN_NOT_ENABLED("premium.admin.not_enabled", "%name"), + + /** Premium mode disabled for %name. Player was online and has been kicked. */ + PREMIUM_ADMIN_DISABLE_SUCCESS("premium.admin.disable_success", "%name"), + + /** An impostor was online as %name and has been kicked. */ + PREMIUM_ADMIN_IMPOSTOR_KICKED("premium.admin.impostor_kicked", "%name"), + + /** Kicked by admin: premium settings changed. */ + PREMIUM_ADMIN_KICK_REASON("premium.admin.kick_reason"), + + /** Kicked to verify premium ownership; player must reconnect. */ + PREMIUM_PENDING_KICK("premium.pending_kick"), + + /** Premium verification failed on reconnect; player must use a password. */ + PREMIUM_PENDING_FAIL("premium.pending_fail"), + + /** Admin notification: verification is pending, player must reconnect to confirm. */ + PREMIUM_ADMIN_PENDING("premium.admin.pending", "%name"); private String key; diff --git a/authme-core/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java b/authme-core/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java index 0aabe66c2..45f2b7c2b 100644 --- a/authme-core/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java +++ b/authme-core/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java @@ -152,6 +152,7 @@ public static MessageKeyConfigurationData createConfigurationData() { .put("dialog", "Dialog UI") .put("command", "Command validation messages") .put("admin", "Admin command messages") + .put("premium", "Premium mode") .build(); Set addedKeys = new HashSet<>(); diff --git a/authme-core/src/main/java/fr/xephi/authme/permission/AdminPermission.java b/authme-core/src/main/java/fr/xephi/authme/permission/AdminPermission.java index 20e27f2fc..5bbb7a7c3 100644 --- a/authme-core/src/main/java/fr/xephi/authme/permission/AdminPermission.java +++ b/authme-core/src/main/java/fr/xephi/authme/permission/AdminPermission.java @@ -138,7 +138,17 @@ public enum AdminPermission implements PermissionNode { /** * Allows to use the backup command. */ - BACKUP("authme.admin.backup"); + BACKUP("authme.admin.backup"), + + /** + * Administrator command to enable premium mode for a player. + */ + SET_PREMIUM("authme.admin.setpremium"), + + /** + * Administrator command to disable premium mode for a player. + */ + SET_FREEMIUM("authme.admin.setfreemium"); /** * The permission node. diff --git a/authme-core/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/authme-core/src/main/java/fr/xephi/authme/permission/PlayerPermission.java index d7427f46a..b1f72b3d9 100644 --- a/authme-core/src/main/java/fr/xephi/authme/permission/PlayerPermission.java +++ b/authme-core/src/main/java/fr/xephi/authme/permission/PlayerPermission.java @@ -83,7 +83,17 @@ public enum PlayerPermission implements PermissionNode { /** * Permission to disable two-factor authentication. */ - DISABLE_TWO_FACTOR_AUTH("authme.player.totpremove"); + DISABLE_TWO_FACTOR_AUTH("authme.player.totpremove"), + + /** + * Permission to enable premium mode (skip authentication using a verified Mojang account). + */ + USE_PREMIUM("authme.player.premium"), + + /** + * Permission to disable premium mode. + */ + USE_FREEMIUM("authme.player.freemium"); /** * The permission node. diff --git a/authme-core/src/main/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapter.java b/authme-core/src/main/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapter.java index 7fa8a08b0..8b8fc2d96 100644 --- a/authme-core/src/main/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapter.java +++ b/authme-core/src/main/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapter.java @@ -7,6 +7,8 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.listener.packetevents.PacketEventsListenerRegistry; import fr.xephi.authme.service.CancellableTask; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumLoginVerifier; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -133,6 +135,30 @@ public void unregisterTabCompleteBlock() { } } + @Override + public void registerPremiumVerification(DataSource dataSource, PremiumLoginVerifier verifier, + PendingPremiumCache pendingPremiumCache) { + getOrCreatePacketInterceptionAdapter() + .registerPremiumVerification(dataSource, verifier, pendingPremiumCache); + } + + @Override + public void unregisterPremiumVerification() { + if (packetInterceptionAdapter != null) { + packetInterceptionAdapter.unregisterPremiumVerification(); + } + } + + @Override + public boolean isProxyForwardingEnabled() { + try { + return Bukkit.getServer().spigot().getConfig() + .getBoolean("settings.bungeecord", false); + } catch (Exception ignored) { + return false; + } + } + protected PacketInterceptionAdapter createPacketInterceptionAdapter() { return new PacketEventsListenerRegistry(); } diff --git a/authme-core/src/main/java/fr/xephi/authme/platform/PacketInterceptionAdapter.java b/authme-core/src/main/java/fr/xephi/authme/platform/PacketInterceptionAdapter.java index 991aa8ebc..d57554e0b 100644 --- a/authme-core/src/main/java/fr/xephi/authme/platform/PacketInterceptionAdapter.java +++ b/authme-core/src/main/java/fr/xephi/authme/platform/PacketInterceptionAdapter.java @@ -2,10 +2,13 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumLoginVerifier; import org.bukkit.entity.Player; /** - * Platform-specific packet interception for inventory protection and tab-complete blocking. + * Platform-specific packet interception for inventory protection, tab-complete blocking, + * and cryptographic premium session verification. * Implementations are provided by each version module and use the PacketEvents library. */ public interface PacketInterceptionAdapter { @@ -19,4 +22,25 @@ public interface PacketInterceptionAdapter { void registerTabCompleteBlock(PlayerCache playerCache); void unregisterTabCompleteBlock(); + + void registerPremiumVerification(DataSource dataSource, PremiumLoginVerifier verifier, + PendingPremiumCache pendingPremiumCache); + + void unregisterPremiumVerification(); + + /** + * Returns {@code true} if the server is configured to receive player connections via a + * BungeeCord or Velocity proxy (i.e., proxy IP forwarding is enabled at the server level). + * When this returns {@code true}, the premium PacketEvents verification listener must NOT + * be registered: the proxy handles the login handshake, and any synthetic + * {@code EncryptionRequest} sent by the backend would cause the proxy to abort the connection + * with "Backend server is online-mode!". + * + *

    The default implementation returns {@code false}. Version adapters override it to inspect + * the server's own proxy-forwarding configuration (e.g. {@code spigot.yml settings.bungeecord} + * or Paper's {@code proxies.velocity.enabled}).

    + */ + default boolean isProxyForwardingEnabled() { + return false; + } } diff --git a/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 528f5a8d0..662f66fb4 100644 --- a/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/authme-core/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -2,6 +2,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; @@ -22,7 +23,10 @@ import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.DialogStateService; import fr.xephi.authme.service.DialogWindowService; +import fr.xephi.authme.service.PendingPremiumCache; import fr.xephi.authme.service.PreJoinDialogService; +import fr.xephi.authme.service.PremiumLoginVerifier; +import fr.xephi.authme.service.PremiumService; import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.service.SessionService; import fr.xephi.authme.service.ValidationService; @@ -31,6 +35,7 @@ import fr.xephi.authme.settings.WelcomeMessageConfiguration; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.PremiumSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.InternetProtocolUtils; @@ -110,6 +115,15 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private PreJoinDialogService preJoinDialogService; + @Inject + private PremiumLoginVerifier premiumLoginVerifier; + + @Inject + private PendingPremiumCache pendingPremiumCache; + + @Inject + private PremiumService premiumService; + AsynchronousJoin() { } @@ -184,6 +198,15 @@ public void processJoin(Player player) { logger.info("The user " + player.getName() + " has been automatically logged in, " + "as present in autologin queue."); return; + } else if (canBypassWithPremium(player)) { + // Premium bypass: player has a verified Mojang UUID matching the connecting player's UUID + if (bungeeSender.isEnabled()) { + // Proxy handles routing; do not attempt backend-side BungeeCord redirect + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLoginFromProxy(player)); + } else { + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); + } + return; } } else if (!service.getProperty(RegistrationSettings.FORCE) && pendingRegistration == null) { bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(player, () -> { @@ -201,6 +224,13 @@ public void processJoin(Player player) { return; } + // Guard: a proxy-initiated forceLoginFromProxy() may have already authenticated the player + // (if the perform.login message arrived and was processed before this async task completes). + // Scheduling a limbo in that case would freeze the player permanently. + if (playerCache.isAuthenticated(name)) { + return; + } + processJoinSync(player, isAuthAvailable, pendingLoginPassword, pendingRegistration, shouldSkipPostJoinDialog, pendingForceLogin); } @@ -227,6 +257,10 @@ private void processJoinSync(Player player, boolean isAuthAvailable, String pend ) * TICKS_PER_SECOND; bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(player, () -> { + // Guard: proxy login may have completed between when this task was scheduled and now + if (playerCache.isAuthenticated(player.getName())) { + return; + } limboService.createLimboPlayer(player, isAuthAvailable); player.setNoDamageTicks(registrationTimeout); @@ -313,6 +347,56 @@ && countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN return true; } + private boolean canBypassWithPremium(Player player) { + if (!service.getProperty(PremiumSettings.ENABLE_PREMIUM)) { + return false; + } + String name = player.getName(); + PlayerAuth auth = database.getAuth(name.toLowerCase(Locale.ROOT)); + if (auth == null) { + return false; + } + + if (!auth.isPremium()) { + // Check for a pending premium verification (player ran /premium and was asked to reconnect). + UUID pendingUuid = pendingPremiumCache.getPendingUuid(name); + if (pendingUuid == null) { + return false; + } + UUID playerId = player.getUniqueId(); + UUID confirmedUuid; + if (playerId.version() == 4) { + // Proxy already performed Mojang authentication — UUID v4 is the confirmed Mojang UUID. + confirmedUuid = playerId.equals(pendingUuid) ? playerId : null; + } else { + // No proxy: require cryptographic session verification via PacketEvents. + UUID verified = premiumLoginVerifier.getVerifiedUuid(name); + confirmedUuid = (verified != null && verified.equals(pendingUuid)) ? verified : null; + } + + pendingPremiumCache.removePending(name); + + if (confirmedUuid != null) { + premiumService.finalizePendingPremium(player, confirmedUuid); + return true; + } else { + bungeeSender.sendPremiumUnset(name); + service.send(player, MessageKey.PREMIUM_PENDING_FAIL); + return false; + } + } + + UUID playerId = player.getUniqueId(); + if (playerId.version() == 4) { + // UUID v4 = Mojang online UUID (online-mode server or proxy forwarding): compare directly. + // Security relies on the backend port being firewalled to only accept proxy connections. + return playerId.equals(auth.getPremiumUuid()); + } + // UUID v3 = Bukkit offline UUID: require cryptographic session verification via PacketEvents. + UUID verifiedUuid = premiumLoginVerifier.getVerifiedUuid(name); + return verifiedUuid != null && verifiedUuid.equals(auth.getPremiumUuid()); + } + private int countOnlinePlayersByIp(String ip) { int count = 0; for (Player player : bukkitService.getOnlinePlayers()) { diff --git a/authme-core/src/main/java/fr/xephi/authme/service/MojangApiService.java b/authme-core/src/main/java/fr/xephi/authme/service/MojangApiService.java new file mode 100644 index 000000000..dffa3d734 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/service/MojangApiService.java @@ -0,0 +1,124 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.util.UuidUtils; + +import javax.inject.Inject; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Centralized HTTP client for Mojang API calls used by the premium feature. + */ +public class MojangApiService { + + private static final String PROFILE_URL = "https://api.mojang.com/users/profiles/minecraft/"; + private static final String HAS_JOINED_URL = + "https://sessionserver.mojang.com/session/minecraft/hasJoined"; + private static final Pattern UUID_PATTERN = + Pattern.compile("\"id\"\\s*:\\s*\"([0-9a-fA-F]{32})\""); + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(MojangApiService.class); + + @Inject + MojangApiService() { + } + + /** + * Queries the Mojang API to resolve the online UUID for a given username. + * + * @param username the player name to look up + * @return the Mojang online UUID, or empty if the account does not exist or an error occurred + */ + public Optional fetchUuidByName(String username) { + try { + HttpURLConnection conn = openGet(PROFILE_URL + username); + int code = conn.getResponseCode(); + if (code == HttpURLConnection.HTTP_NO_CONTENT || code == HttpURLConnection.HTTP_NOT_FOUND) { + return Optional.empty(); + } + if (code == 429) { + logger.warning("Mojang profile API rate-limited (429) for '" + username + "'; try again later"); + return Optional.empty(); + } + if (code != HttpURLConnection.HTTP_OK) { + logger.warning("Mojang profile API returned " + code + " for '" + username + "'"); + return Optional.empty(); + } + return parseUuid(readBody(conn), username); + } catch (IOException e) { + logger.warning("Failed to contact Mojang profile API for '" + username + "': " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * Calls Mojang's {@code hasJoined} session endpoint to verify a premium login. + * + * @param username the player name + * @param serverHash the Minecraft server hash (two's-complement SHA-1 hex) + * @return the Mojang online UUID on a valid session, or empty on any failure + */ + public Optional hasJoined(String username, String serverHash) { + try { + String url = HAS_JOINED_URL + "?username=" + username + "&serverId=" + serverHash; + HttpURLConnection conn = openGet(url); + int code = conn.getResponseCode(); + if (code == HttpURLConnection.HTTP_NO_CONTENT || code == HttpURLConnection.HTTP_NOT_FOUND) { + return Optional.empty(); + } + if (code != HttpURLConnection.HTTP_OK) { + logger.warning("Mojang hasJoined returned " + code + " for '" + username + "'"); + return Optional.empty(); + } + return parseUuid(readBody(conn), username); + } catch (IOException e) { + logger.warning("Failed to contact Mojang session server for '" + username + "': " + e.getMessage()); + return Optional.empty(); + } + } + + private HttpURLConnection openGet(String urlStr) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + return conn; + } + + private Optional parseUuid(String body, String username) { + Matcher matcher = UUID_PATTERN.matcher(body); + if (!matcher.find()) { + return Optional.empty(); + } + String raw = matcher.group(1); + String dashed = raw.substring(0, 8) + "-" + raw.substring(8, 12) + "-" + + raw.substring(12, 16) + "-" + raw.substring(16, 20) + "-" + raw.substring(20); + UUID uuid = UuidUtils.parseUuidSafely(dashed); + if (uuid == null) { + logger.warning("Mojang returned an unparseable UUID for '" + username + "': " + raw); + } + return Optional.ofNullable(uuid); + } + + private static String readBody(HttpURLConnection conn) throws IOException { + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } + return sb.toString(); + } +} diff --git a/authme-core/src/main/java/fr/xephi/authme/service/PendingPremiumCache.java b/authme-core/src/main/java/fr/xephi/authme/service/PendingPremiumCache.java new file mode 100644 index 000000000..cf41decf8 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/service/PendingPremiumCache.java @@ -0,0 +1,108 @@ +package fr.xephi.authme.service; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Holds in-memory premium verification requests that have not yet been confirmed by a + * cryptographic Mojang session check. Entries expire after {@link #TTL_MS} (5 minutes). + * + *

    A player is placed here when they run {@code /premium} (or an admin runs + * {@code /authme premium}) on an offline-mode backend. The entry is consumed (and the + * UUID persisted to the database) when the player reconnects and the Mojang session check + * succeeds. If the check fails, the entry is removed without saving.

    + */ +public class PendingPremiumCache { + + static final long TTL_MS = 5 * 60 * 1000L; + + private final ConcurrentHashMap pending = new ConcurrentHashMap<>(); + + @Inject + PendingPremiumCache() { + } + + /** + * Registers a pending premium verification for the given player name. + * Also evicts any already-expired entries. + * + * @param name the player name (case-insensitive) + * @param mojangUuid the Mojang UUID fetched from the profile API + * @return the names of any entries that were evicted due to expiry + */ + public Collection addPending(String name, UUID mojangUuid) { + Collection evicted = evictExpired(); + pending.put(name.toLowerCase(Locale.ROOT), + new PendingEntry(mojangUuid, System.currentTimeMillis() + TTL_MS)); + return evicted; + } + + /** + * Returns whether a non-expired pending entry exists for the given player name. + * + * @param name the player name (case-insensitive) + * @return true if a pending entry exists and has not yet expired + */ + public boolean isPending(String name) { + return getPendingUuid(name) != null; + } + + /** + * Returns the Mojang UUID associated with the pending entry for the given player name, + * or {@code null} if no entry exists or it has expired. + * + * @param name the player name (case-insensitive) + * @return the pending Mojang UUID, or null + */ + public UUID getPendingUuid(String name) { + PendingEntry entry = pending.get(name.toLowerCase(Locale.ROOT)); + if (entry == null) { + return null; + } + if (System.currentTimeMillis() > entry.expiresAt()) { + pending.remove(name.toLowerCase(Locale.ROOT)); + return null; + } + return entry.mojangUuid(); + } + + /** + * Removes and returns the pending Mojang UUID for the given player name. + * + * @param name the player name (case-insensitive) + * @return the pending Mojang UUID if a valid entry existed, or null + */ + public UUID removePending(String name) { + PendingEntry entry = pending.remove(name.toLowerCase(Locale.ROOT)); + if (entry == null || System.currentTimeMillis() > entry.expiresAt()) { + return null; + } + return entry.mojangUuid(); + } + + /** + * Removes all expired entries and returns their player names so the caller can + * notify the proxy (via {@code PREMIUM_UNSET}) that the pending state is gone. + * + * @return the names of evicted entries (lowercase) + */ + public Collection evictExpired() { + long now = System.currentTimeMillis(); + List evicted = new ArrayList<>(); + pending.entrySet().removeIf(e -> { + if (now > e.getValue().expiresAt()) { + evicted.add(e.getKey()); + return true; + } + return false; + }); + return evicted; + } + + private record PendingEntry(UUID mojangUuid, long expiresAt) {} +} diff --git a/authme-core/src/main/java/fr/xephi/authme/service/PremiumLoginVerifier.java b/authme-core/src/main/java/fr/xephi/authme/service/PremiumLoginVerifier.java new file mode 100644 index 000000000..1cbd7d36a --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/service/PremiumLoginVerifier.java @@ -0,0 +1,209 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +import javax.crypto.Cipher; +import javax.inject.Inject; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Performs cryptographic premium session verification. + * + *

    Generates a per-server RSA key pair and drives an EncryptionRequest / EncryptionResponse + * handshake with connecting clients so that the backend can independently verify premium identity + * via Mojang's {@code hasJoined} session endpoint — without relying on the proxy.

    + * + *

    Thread-safety: all state maps are {@link ConcurrentHashMap}; RSA operations use a + * per-call {@link Cipher} instance.

    + */ +public class PremiumLoginVerifier { + + private static final long VERIFIED_TTL_MS = 60_000L; + private static final long PENDING_TTL_MS = 30_000L; + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PremiumLoginVerifier.class); + + private final KeyPair rsaKeyPair; + private final SecureRandom secureRandom; + private final MojangApiService mojangApiService; + private final BukkitService bukkitService; + + /** Keyed by connection address (ip:port). */ + private final ConcurrentHashMap pending = new ConcurrentHashMap<>(); + /** Keyed by lowercase username; entries expire after {@link #VERIFIED_TTL_MS}. */ + private final ConcurrentHashMap verified = new ConcurrentHashMap<>(); + + @Inject + PremiumLoginVerifier(MojangApiService mojangApiService, BukkitService bukkitService) { + this.mojangApiService = mojangApiService; + this.bukkitService = bukkitService; + this.secureRandom = new SecureRandom(); + try { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(1024, secureRandom); + this.rsaKeyPair = gen.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("RSA algorithm not available", e); + } + } + + /** Returns the server's RSA public key to embed in the EncryptionRequest packet. */ + public PublicKey getPublicKey() { + return rsaKeyPair.getPublic(); + } + + /** + * Begins a verification handshake for the given connection. + * + * @param playerUUID the UUID from the LOGIN_START packet (null on older protocol versions) + * @return a freshly generated 4-byte verify token to embed in the EncryptionRequest + */ + public byte[] startVerification(String connectionKey, String username, UUID playerUUID) { + evictStalePendingEntries(); + byte[] verifyToken = new byte[4]; + secureRandom.nextBytes(verifyToken); + pending.put(connectionKey, new PendingVerification(username, playerUUID, verifyToken, System.currentTimeMillis())); + return verifyToken; + } + + private void evictStalePendingEntries() { + long now = System.currentTimeMillis(); + pending.entrySet().removeIf(e -> now - e.getValue().startedAt() > PENDING_TTL_MS); + } + + /** Returns true if an in-flight verification exists for this connection. */ + public boolean hasPending(String connectionKey) { + return pending.containsKey(connectionKey); + } + + /** Returns the username associated with the pending verification, or {@code null}. */ + public String getPendingUsername(String connectionKey) { + PendingVerification pend = pending.get(connectionKey); + return pend != null ? pend.username() : null; + } + + /** Returns the player UUID from the pending LOGIN_START packet, or {@code null}. */ + public UUID getPendingPlayerUUID(String connectionKey) { + PendingVerification pend = pending.get(connectionKey); + return pend != null ? pend.playerUUID() : null; + } + + /** Removes the pending verification for the given connection. */ + public void cleanupPending(String connectionKey) { + pending.remove(connectionKey); + } + + /** + * Decrypts a byte array using the server's RSA private key. + * Used by the packet listener to decrypt the shared secret before setting up Netty + * encryption, independently from the async Mojang check. + */ + public byte[] decryptData(byte[] encrypted) throws GeneralSecurityException { + return rsaDecrypt(encrypted); + } + + /** + * Completes a verification handshake: validates the verify token and calls Mojang's + * {@code hasJoined} endpoint asynchronously. + * + *

    The caller is responsible for RSA-decrypting {@code sharedSecret} before calling + * this method (so that the Netty AES handlers can be installed synchronously on the + * event-loop thread before this async operation starts).

    + * + * @param connectionKey key used in {@link #startVerification} + * @param sharedSecret already RSA-decrypted AES shared secret from the client + * @param encVerifyToken RSA-encrypted verify token from the client + * @return a future resolving to the Mojang UUID on success, or empty on any failure + */ + public CompletableFuture> completeVerification( + String connectionKey, byte[] sharedSecret, byte[] encVerifyToken) { + PendingVerification pend = pending.remove(connectionKey); + if (pend == null) { + return CompletableFuture.completedFuture(Optional.empty()); + } + CompletableFuture> future = new CompletableFuture<>(); + bukkitService.runTaskAsynchronously(() -> { + try { + byte[] decryptedToken = rsaDecrypt(encVerifyToken); + + if (!Arrays.equals(decryptedToken, pend.verifyToken())) { + logger.warning("Verify token mismatch during premium verification for '" + pend.username() + "'"); + future.complete(Optional.empty()); + return; + } + + String serverHash = computeServerHash(sharedSecret); + future.complete(mojangApiService.hasJoined(pend.username(), serverHash)); + + } catch (Exception e) { + logger.warning("Premium session verification failed for '" + pend.username() + "': " + e.getMessage()); + future.complete(Optional.empty()); + } + }); + return future; + } + + /** + * Stores a successfully verified Mojang UUID for the given username. + * Retrieved later by {@link #getVerifiedUuid} during the join flow. + */ + public void storeVerified(String username, UUID mojangUuid) { + verified.put(username.toLowerCase(Locale.ROOT), + new VerifiedSession(mojangUuid, System.currentTimeMillis())); + } + + /** + * Returns the Mojang-confirmed UUID for the given username if a valid (non-expired) verified + * session exists, or {@code null} otherwise. + */ + public UUID getVerifiedUuid(String username) { + String key = username.toLowerCase(Locale.ROOT); + VerifiedSession session = verified.get(key); + if (session == null) { + return null; + } + if (System.currentTimeMillis() - session.verifiedAt() > VERIFIED_TTL_MS) { + verified.remove(key); + return null; + } + return session.mojangUuid(); + } + + private byte[] rsaDecrypt(byte[] encrypted) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, rsaKeyPair.getPrivate()); + return cipher.doFinal(encrypted); + } + + private String computeServerHash(byte[] sharedSecret) throws NoSuchAlgorithmException { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update("".getBytes(StandardCharsets.ISO_8859_1)); // empty server ID (MC 1.7+) + sha1.update(sharedSecret); + sha1.update(rsaKeyPair.getPublic().getEncoded()); + // Minecraft uses two's-complement BigInteger hex (may have leading minus sign) + return new BigInteger(sha1.digest()).toString(16); + } + + // --------------------------------------------------------------------------- + // Internal record types + // --------------------------------------------------------------------------- + + record PendingVerification(String username, UUID playerUUID, byte[] verifyToken, long startedAt) {} + + record VerifiedSession(UUID mojangUuid, long verifiedAt) {} +} diff --git a/authme-core/src/main/java/fr/xephi/authme/service/PremiumService.java b/authme-core/src/main/java/fr/xephi/authme/service/PremiumService.java new file mode 100644 index 000000000..8c1858d4b --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/service/PremiumService.java @@ -0,0 +1,302 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PremiumSettings; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +/** + * Handles premium mode — allowing players with an official Minecraft account to skip authentication. + */ +public class PremiumService implements HasCleanup { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PremiumService.class); + + @Inject + private Settings settings; + + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + + @Inject + private Messages messages; + + @Inject + private BukkitService bukkitService; + + @Inject + private BungeeSender bungeeSender; + + @Inject + private MojangApiService mojangApiService; + + @Inject + private PendingPremiumCache pendingPremiumCache; + + PremiumService() { + } + + /** + * Enables premium mode for the given player. Verifies the player is logged in, that the feature + * is enabled, and that the player has an official Minecraft account via the Mojang API. + * On success, stores the Mojang UUID as {@code premium_uuid} in the database. + * + * @param player the player requesting premium mode + */ + public void enablePremium(Player player) { + if (!settings.getProperty(PremiumSettings.ENABLE_PREMIUM)) { + messages.send(player, MessageKey.PREMIUM_FEATURE_DISABLED); + return; + } + + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.NOT_LOGGED_IN); + return; + } + + if (auth.isPremium()) { + messages.send(player, MessageKey.PREMIUM_ALREADY_ENABLED); + return; + } + + UUID playerUuid = player.getUniqueId(); + if (playerUuid.version() == 4) { + // UUID v4 = Mojang-issued UUID: either the server is in online mode (Mojang already + // verified the session at the connection level) or the proxy forwarded a real Mojang + // UUID. No extra API call needed — the server's own auth is the source of truth. + storePremiumUuid(auth, playerUuid, player, player.getName()); + } else { + // UUID v3 = Bukkit offline UUID: fetch the Mojang UUID and hold it as pending. + // The player must reconnect; a cryptographic Mojang session check on reconnect is + // what actually confirms ownership before we write anything to the database. + String playerName = player.getName(); + bukkitService.runTaskOptionallyAsync(() -> { + Optional mojangUuid = mojangApiService.fetchUuidByName(playerName); + if (!mojangUuid.isPresent()) { + messages.send(player, MessageKey.PREMIUM_ACCOUNT_NOT_FOUND); + return; + } + Collection evicted = pendingPremiumCache.addPending(playerName, mojangUuid.get()); + evicted.forEach(bungeeSender::sendPremiumUnset); + bungeeSender.sendPremiumPendingSet(playerName); + String kickMsg = messages.retrieveSingle(playerName, MessageKey.PREMIUM_PENDING_KICK); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(player, + () -> player.kickPlayer(kickMsg)); + }); + } + } + + /** + * Disables premium mode for the given player. + * + * @param player the player requesting to disable premium mode + */ + public void disablePremium(Player player) { + if (!settings.getProperty(PremiumSettings.ENABLE_PREMIUM)) { + messages.send(player, MessageKey.PREMIUM_FEATURE_DISABLED); + return; + } + + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.NOT_LOGGED_IN); + return; + } + + if (!auth.isPremium()) { + messages.send(player, MessageKey.PREMIUM_NOT_ENABLED); + return; + } + + auth.setPremiumUuid(null); + String playerName = player.getName(); + bukkitService.runTaskOptionallyAsync(() -> { + if (dataSource.updatePremiumUuid(auth)) { + playerCache.updatePlayer(auth); + bungeeSender.sendPremiumUnset(playerName); + messages.send(player, MessageKey.PREMIUM_DISABLE_SUCCESS); + } else { + logger.warning("Failed to clear premium UUID for player " + playerName); + messages.send(player, MessageKey.PREMIUM_ERROR); + } + }); + } + + /** + * Enables premium mode for the given player name, callable by an admin from the console or in-game. + * Verifies that the feature is enabled, that the player is registered, that they don't already have premium, + * and that a Mojang account exists for the name. If an impostor (a player with the same name but a different + * UUID) is currently online, they are kicked before the premium UUID is saved. + * + * @param sender the command sender (admin or console) + * @param playerName the name of the player to enable premium for + */ + public void enablePremiumAdmin(CommandSender sender, String playerName) { + if (!settings.getProperty(PremiumSettings.ENABLE_PREMIUM)) { + messages.send(sender, MessageKey.PREMIUM_FEATURE_DISABLED); + return; + } + + bukkitService.runTaskOptionallyAsync(() -> { + PlayerAuth auth = dataSource.getAuth(playerName); + if (auth == null) { + messages.send(sender, MessageKey.PREMIUM_ADMIN_NOT_REGISTERED, playerName); + return; + } + + if (auth.isPremium()) { + messages.send(sender, MessageKey.PREMIUM_ADMIN_ALREADY_ENABLED, playerName); + return; + } + + Optional mojangUuid = mojangApiService.fetchUuidByName(playerName); + if (!mojangUuid.isPresent()) { + messages.send(sender, MessageKey.PREMIUM_ADMIN_ACCOUNT_NOT_FOUND, playerName); + return; + } + + // If someone is online with that name but a different UUID, they are an impostor — kick them + Player onlinePlayer = bukkitService.getPlayerExact(playerName); + if (onlinePlayer != null && !mojangUuid.get().equals(onlinePlayer.getUniqueId())) { + String kickMsg = messages.retrieveSingle(playerName, MessageKey.PREMIUM_ADMIN_KICK_REASON); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(onlinePlayer, + () -> onlinePlayer.kickPlayer(kickMsg)); + messages.send(sender, MessageKey.PREMIUM_ADMIN_IMPOSTOR_KICKED, playerName); + } + + // If the online player has a UUID v4 equal to the Mojang UUID the server/proxy already + // verified their identity; save directly. Otherwise hold as pending and require the + // player to reconnect so the crypto Mojang session check can confirm ownership. + if (onlinePlayer != null + && onlinePlayer.getUniqueId().version() == 4 + && mojangUuid.get().equals(onlinePlayer.getUniqueId())) { + auth.setPremiumUuid(mojangUuid.get()); + if (dataSource.updatePremiumUuid(auth)) { + playerCache.updatePlayer(auth); + bungeeSender.sendPremiumSet(playerName); + messages.send(sender, MessageKey.PREMIUM_ADMIN_ENABLE_SUCCESS, playerName); + } else { + logger.warning("Failed to save premium UUID for player " + playerName); + messages.send(sender, MessageKey.PREMIUM_ERROR); + } + } else { + Collection evicted = pendingPremiumCache.addPending(playerName, mojangUuid.get()); + evicted.forEach(bungeeSender::sendPremiumUnset); + bungeeSender.sendPremiumPendingSet(playerName); + messages.send(sender, MessageKey.PREMIUM_ADMIN_PENDING, playerName); + if (onlinePlayer != null) { + String kickMsg = messages.retrieveSingle(playerName, MessageKey.PREMIUM_PENDING_KICK); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(onlinePlayer, + () -> onlinePlayer.kickPlayer(kickMsg)); + } + } + }); + } + + /** + * Disables premium mode for the given player name, callable by an admin from the console or in-game. + * If the player is currently online (regardless of whether their UUID matches the stored premium UUID), + * they are kicked so they must reauthenticate with a password on their next connection. + * + * @param sender the command sender (admin or console) + * @param playerName the name of the player to disable premium for + */ + public void disablePremiumAdmin(CommandSender sender, String playerName) { + if (!settings.getProperty(PremiumSettings.ENABLE_PREMIUM)) { + messages.send(sender, MessageKey.PREMIUM_FEATURE_DISABLED); + return; + } + + bukkitService.runTaskOptionallyAsync(() -> { + PlayerAuth auth = dataSource.getAuth(playerName); + if (auth == null) { + messages.send(sender, MessageKey.PREMIUM_ADMIN_NOT_REGISTERED, playerName); + return; + } + + if (!auth.isPremium()) { + messages.send(sender, MessageKey.PREMIUM_ADMIN_NOT_ENABLED, playerName); + return; + } + + auth.setPremiumUuid(null); + if (!dataSource.updatePremiumUuid(auth)) { + logger.warning("Failed to clear premium UUID for player " + playerName); + messages.send(sender, MessageKey.PREMIUM_ERROR); + return; + } + + bungeeSender.sendPremiumUnset(playerName); + if (playerCache.isAuthenticated(playerName)) { + playerCache.updatePlayer(auth); + } + + Player onlinePlayer = bukkitService.getPlayerExact(playerName); + if (onlinePlayer != null) { + String kickMsg = messages.retrieveSingle(playerName, MessageKey.PREMIUM_ADMIN_KICK_REASON); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(onlinePlayer, + () -> onlinePlayer.kickPlayer(kickMsg)); + } + + messages.send(sender, MessageKey.PREMIUM_ADMIN_DISABLE_SUCCESS, playerName); + }); + } + + @Override + public void performCleanup() { + Collection evicted = pendingPremiumCache.evictExpired(); + evicted.forEach(bungeeSender::sendPremiumUnset); + } + + /** + * Finalizes a pending premium verification: saves the confirmed Mojang UUID to the database, + * updates the player cache, notifies the proxy, and sends a success message to the player. + * Must be called from an async context. + * + * @param player the player whose premium status is being confirmed + * @param confirmedUuid the Mojang UUID that was cryptographically verified + */ + public void finalizePendingPremium(Player player, UUID confirmedUuid) { + String playerName = player.getName(); + PlayerAuth auth = playerCache.getAuth(playerName); + if (auth == null) { + auth = dataSource.getAuth(playerName.toLowerCase(java.util.Locale.ROOT)); + } + if (auth == null) { + logger.warning("Could not finalize pending premium for " + playerName + ": no auth record found"); + return; + } + storePremiumUuid(auth, confirmedUuid, player, playerName); + } + + private void storePremiumUuid(PlayerAuth auth, UUID uuid, CommandSender feedbackTarget, String playerName) { + auth.setPremiumUuid(uuid); + if (dataSource.updatePremiumUuid(auth)) { + playerCache.updatePlayer(auth); + bungeeSender.sendPremiumSet(playerName); + messages.send(feedbackTarget, MessageKey.PREMIUM_ENABLE_SUCCESS); + } else { + logger.warning("Failed to save premium UUID for player " + playerName); + messages.send(feedbackTarget, MessageKey.PREMIUM_ERROR); + } + } + +} 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 4707e2983..334a7aca4 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 @@ -5,11 +5,14 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.HashUtils; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import org.bukkit.entity.Player; @@ -17,7 +20,9 @@ import org.bukkit.plugin.messaging.PluginMessageListener; import javax.inject.Inject; +import java.util.List; import java.util.Optional; +import java.util.UUID; public class BungeeReceiver implements PluginMessageListener, SettingsDependent { @@ -29,6 +34,9 @@ public class BungeeReceiver implements PluginMessageListener, SettingsDependent private final ProxySessionManager proxySessionManager; private final Management management; private final BungeeSender bungeeSender; + private final DataSource dataSource; + private final PendingPremiumCache pendingPremiumCache; + private final PremiumService premiumService; private static final String AUTHME_CHANNEL = "authme:main"; private static final long MAX_AGE_MILLIS = 30_000L; @@ -38,12 +46,17 @@ public class BungeeReceiver implements PluginMessageListener, SettingsDependent @Inject BungeeReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager, - Management management, BungeeSender bungeeSender, Settings settings) { + Management management, BungeeSender bungeeSender, DataSource dataSource, + PendingPremiumCache pendingPremiumCache, PremiumService premiumService, + Settings settings) { this.plugin = plugin; this.bukkitService = bukkitService; this.proxySessionManager = proxySessionManager; this.management = management; this.bungeeSender = bungeeSender; + this.dataSource = dataSource; + this.pendingPremiumCache = pendingPremiumCache; + this.premiumService = premiumService; reload(settings); } @@ -87,13 +100,23 @@ public void onPluginMessageReceived(String channel, Player player, byte[] data) try { argument = in.readUTF(); } catch (IllegalStateException e) { - logger.warning("Received invalid AuthMe plugin message of type " + type.get().name() - + ": argument is missing!"); + logger.warning("Received invalid AuthMe plugin message of type " + type.get().name() + ": argument is missing!"); return; } 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 carrier = player; + bukkitService.runTaskAsynchronously(() -> { + List premiumNames = dataSource.getPremiumUsernames(); + if (!premiumNames.isEmpty()) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + bungeeSender.sendPremiumList(carrier, premiumNames); + logger.info("Sent premium list (" + premiumNames.size() + " player(s)) to proxy '" + proxyName + "'"); + }); + } + }); return; } @@ -128,17 +151,33 @@ private boolean verifyHmac(String playerName, long timestamp, String providedHma } private void performLogin(String name) { - logger.debug("Received perform.login request for " + name); + logger.debug("Received perform.login request for {0}", name); // Always queue in the proxy session manager so processJoin can consume it even when // the player is already online (PlayerJoinEvent fires before ServerSwitchEvent on the // proxy, so processJoin may run before perform.login arrives at this backend). proxySessionManager.processProxySessionMessage(name); Player player = bukkitService.getPlayerExact(name); if (player != null && player.isOnline()) { + // If the player has a pending premium verification, the proxy has already confirmed + // their Mojang identity via its own online-mode handshake (UUID v4). Finalize the + // verification before force-logging them in so the UUID is persisted to the database. + UUID pendingUuid = pendingPremiumCache.removePending(name); + if (pendingUuid != null) { + UUID playerId = player.getUniqueId(); + if (playerId.version() == 4) { + premiumService.finalizePendingPremium(player, playerId); + } else { + // Unexpected: proxy sent PERFORM_LOGIN but player has an offline UUID. + // Discard the pending state and let the player use a password. + logger.warning("Received PERFORM_LOGIN for pending-premium player " + name + + " but their UUID is not v4 — discarding pending verification"); + bungeeSender.sendPremiumUnset(name); + } + } // Player is already online: also drive the login directly in case processJoin // has already run past the proxy-session check and created a limbo player. management.forceLoginFromProxy(player); - logger.debug("Sending auto-login ACK for " + player.getName()); + logger.debug("Sending auto-login ACK for {0}", player.getName()); bungeeSender.sendAuthMeBungeecordMessage(player, MessageType.PERFORM_LOGIN_ACK); logger.info(player.getName() + " has been automatically logged in via proxy request."); } else { diff --git a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java index 7761f2085..db389f52e 100644 --- a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java +++ b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java @@ -3,6 +3,9 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import fr.xephi.authme.AuthMe; + +import java.util.List; +import java.util.Optional; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.output.ConsoleLoggerFactory; @@ -13,7 +16,6 @@ import org.bukkit.plugin.messaging.Messenger; import javax.inject.Inject; -import java.util.Locale; public class BungeeSender implements SettingsDependent { @@ -96,8 +98,76 @@ public void sendAuthMeBungeecordMessage(Player player, MessageType type) { } ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF(type.getId()); - out.writeUTF(player.getName().toLowerCase(Locale.ROOT)); + out.writeUTF(player.getName().toLowerCase(java.util.Locale.ROOT)); bukkitService.sendAuthMePluginMessage(player, out.toByteArray()); } + /** + * Notifies the proxy that the given username has been enrolled as premium. + * Uses any online player as the message carrier. + * + * @param username the player name (will be lowercased) + */ + public void sendPremiumSet(String username) { + sendPremiumNotification(MessageType.PREMIUM_SET, username); + } + + /** + * Notifies the proxy that a pending premium verification has been started for the given + * username. The proxy should force Mojang authentication for this player on their next + * connection but must NOT auto-login them via {@code PERFORM_LOGIN} (that path is reserved + * for confirmed premium players). Uses any online player as the message carrier. + * + * @param username the player name (will be lowercased) + */ + public void sendPremiumPendingSet(String username) { + sendPremiumNotification(MessageType.PREMIUM_PENDING_SET, username); + } + + /** + * Notifies the proxy that premium mode has been disabled for the given username. + * Uses any online player as the message carrier. + * + * @param username the player name (will be lowercased) + */ + public void sendPremiumUnset(String username) { + sendPremiumNotification(MessageType.PREMIUM_UNSET, username); + } + + /** + * Sends the full list of premium usernames to the proxy using the given carrier player. + * Should be called when the proxy starts up (after receiving {@code proxy.started}). + * + * @param carrier the player used as message carrier + * @param premiumUsernames the list of premium usernames (lowercase) + */ + public void sendPremiumList(Player carrier, List premiumUsernames) { + if (!isEnabled || !plugin.isEnabled()) { + return; + } + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF(MessageType.PREMIUM_LIST.getId()); + out.writeUTF(String.join(",", premiumUsernames)); + bukkitService.sendAuthMePluginMessage(carrier, out.toByteArray()); + } + + private void sendPremiumNotification(MessageType type, String username) { + if (!isEnabled || !plugin.isEnabled()) { + return; + } + Optional carrier = bukkitService.getOnlinePlayers().stream().findFirst(); + if (!carrier.isPresent()) { + logger.warning("Cannot send premium notification to proxy: no online player available as carrier." + + " Premium state may be stale on the proxy until the next full resync."); + return; + } + Player p = carrier.get(); + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF(type.getId()); + out.writeUTF(username.toLowerCase(java.util.Locale.ROOT)); + byte[] payload = out.toByteArray(); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> + bukkitService.sendAuthMePluginMessage(p, payload)); + } + } diff --git a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java index ce1ba6192..d1333e16c 100644 --- a/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java +++ b/authme-core/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java @@ -7,7 +7,11 @@ public enum MessageType { LOGOUT("logout", true), PERFORM_LOGIN("perform.login", false), PERFORM_LOGIN_ACK("perform.login.ack", false), - PROXY_STARTED("proxy.started", false); + PROXY_STARTED("proxy.started", false), + PREMIUM_SET("premium.set", false), + PREMIUM_UNSET("premium.unset", false), + PREMIUM_LIST("premium.list", false), + PREMIUM_PENDING_SET("premium.pending.set", false); private final String id; private final boolean broadcast; diff --git a/authme-core/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java b/authme-core/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java index 34a91a111..d9c8b7a66 100644 --- a/authme-core/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java +++ b/authme-core/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java @@ -23,6 +23,6 @@ public static ConfigurationData buildConfigurationData() { DatabaseSettings.class, PluginSettings.class, RestrictionSettings.class, EmailSettings.class, HooksSettings.class, ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class, RegistrationSettings.class, - LimboSettings.class, BackupSettings.class); + LimboSettings.class, BackupSettings.class, PremiumSettings.class); } } diff --git a/authme-core/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/authme-core/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 0792d9d74..e83258672 100644 --- a/authme-core/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/authme-core/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -138,6 +138,11 @@ public final class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_PLAYER_UUID = newProperty( "DataSource.mySQLPlayerUUID", "" ); + @Comment({"Column for storing the Mojang UUID of premium players", + "(null if premium mode is off for this player)"}) + public static final Property MYSQL_COL_PREMIUM_UUID = + newProperty("DataSource.mySQLColumnPremiumUUID", "premiumUUID"); + @Comment("Column for storing players groups") public static final Property MYSQL_COL_GROUP = newProperty("ExternalBoardOptions.mySQLColumnGroup", ""); diff --git a/authme-core/src/main/java/fr/xephi/authme/settings/properties/PremiumSettings.java b/authme-core/src/main/java/fr/xephi/authme/settings/properties/PremiumSettings.java new file mode 100644 index 000000000..e6a87e281 --- /dev/null +++ b/authme-core/src/main/java/fr/xephi/authme/settings/properties/PremiumSettings.java @@ -0,0 +1,27 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class PremiumSettings implements SettingsHolder { + + @Comment({ + "Enable premium mode: players with an official Minecraft account", + "can skip password authentication.", + "Verification method is chosen automatically:", + " - online-mode=true: Bukkit already has the Mojang UUID; no PacketEvents needed.", + " - offline-mode + proxy: set Hooks.bungeecord=true; UUID is forwarded by proxy.", + " - offline-mode, no proxy: PacketEvents required for cryptographic verification.", + " Without PacketEvents, premium auto-login is disabled (fail closed).", + "Players must use /premium to opt in." + }) + public static final Property ENABLE_PREMIUM = + newProperty("settings.enablePremium", false); + + private PremiumSettings() { + } + +} diff --git a/authme-core/src/main/resources/messages/messages_bg.yml b/authme-core/src/main/resources/messages/messages_bg.yml index d254fc7b5..a8fbd8d88 100644 --- a/authme-core/src/main/resources/messages/messages_bg.yml +++ b/authme-core/src/main/resources/messages/messages_bg.yml @@ -213,3 +213,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Режим Premium +premium: + feature_disabled: '&cРежимът Premium не е активиран на този сървър.' + account_not_found: '&cНе е намерен премиум Minecraft акаунт за вашето потребителско име.' + already_enabled: '&eРежимът Premium вече е активиран за вашия акаунт.' + enable_success: '&2Режимът Premium е активиран! Вече няма да е необходимо да се удостоверявате при влизане.' + not_enabled: '&eРежимът Premium не е активиран за вашия акаунт.' + disable_success: '&2Режимът Premium е деактивиран. Ще трябва отново да се удостоверявате.' + error: '&cВъзникна грешка при проверката на вашия Premium статус. Моля, опитайте отново по-късно.' + admin: + not_registered: '&c%name не е регистриран.' + already_enabled: '&eРежимът Premium вече е активиран за %name.' + account_not_found: '&cНе е намерен Mojang акаунт за %name.' + enable_success: '&2Режимът Premium е активиран за %name.' + not_enabled: '&eРежимът Premium не е активиран за %name.' + disable_success: '&2Режимът Premium е деактивиран за %name.' + impostor_kicked: '&eИграч, влязъл като %name, е имал несъответстващ UUID и е бил изхвърлен.' + kick_reason: '&cПремиум настройките на вашия акаунт бяха променени от администратор. Моля, свържете се отново.' + pending: '&eПремиум верификацията е в изчакване за %name. Те трябва да се свържат отново, за да потвърдят собствеността на акаунта в Mojang.' + pending_kick: '&eПоискана е премиум верификация. Моля, свържете се отново, за да потвърдите собствеността на вашия акаунт в Mojang.' + pending_fail: '&cПремиум верификацията не бе успешна. Моля, влезте с вашата парола.' diff --git a/authme-core/src/main/resources/messages/messages_br.yml b/authme-core/src/main/resources/messages/messages_br.yml index 00ce73663..683b9177a 100644 --- a/authme-core/src/main/resources/messages/messages_br.yml +++ b/authme-core/src/main/resources/messages/messages_br.yml @@ -1,56 +1,56 @@ -# AuthMe Reloaded | Tradução pt-br +# AuthMe Reloaded | Tradução pt-br # Por Eufranio, GabrielDev (DeathRush) e RenanYudi # # Lista de tags globais: # %nl% - Pula uma linha -# %username% - Substitui pelo nome do jogador que está recebendo a mensagem -# %displayname% - Substitui pelo nickname (e cores) do jogador que está recebendo a mensagem +# %username% - Substitui pelo nome do jogador que está recebendo a mensagem +# %displayname% - Substitui pelo nickname (e cores) do jogador que está recebendo a mensagem # Registro registration: register_request: '&3Por favor, registre-se com o comando "/register "' command_usage: '&cUse: /register ' - reg_only: '&4Somente usuários registrados podem entrar no servidor! Por favor visite www.seusite.com para se registrar!' + reg_only: '&4Somente usuários registrados podem entrar no servidor! Por favor visite www.seusite.com para se registrar!' success: '&2Registrado com sucesso!' - kicked_admin_registered: 'Um administrador registrou você. Por favor, faça login novamente' - disabled: '&cRegistrar-se está desativado neste servidor!' - name_taken: '&cVocê já registrou este nome de usuário!' + kicked_admin_registered: 'Um administrador registrou você. Por favor, faça login novamente' + disabled: '&cRegistrar-se está desativado neste servidor!' + name_taken: '&cVocê já registrou este nome de usuário!' # Erros de senha ao registrar-se password: - match_error: '&cAs senhas não coincidem, tente novamente!' - name_in_password: '&Você não pode usar o seu nome como senha. Por favor, escolha outra senha...' - unsafe_password: '&cA senha escolhida não é segura. Por favor, escolha outra senha...' - forbidden_characters: '&Sua senha contém caracteres inválidos. Caracteres permitidos: %valid_chars' - wrong_length: '&cSua senha é muito curta ou muito longa! Por favor, escolha outra senha...' + match_error: '&cAs senhas não coincidem, tente novamente!' + name_in_password: '&Você não pode usar o seu nome como senha. Por favor, escolha outra senha...' + unsafe_password: '&cA senha escolhida não é segura. Por favor, escolha outra senha...' + forbidden_characters: '&Sua senha contém caracteres inválidos. Caracteres permitidos: %valid_chars' + wrong_length: '&cSua senha é muito curta ou muito longa! Por favor, escolha outra senha...' # Login login: command_usage: '&cUse: /login ' wrong_password: '&cSenha incorreta!' success: '&2Login realizado com sucesso!' - login_request: '&cPor favor, faça login com o comando "/login "' + login_request: '&cPor favor, faça login com o comando "/login "' timeout_error: '&4Tempo limite excedido.' # Erros error: - unregistered_user: '&cEste usuário não está registrado!' - denied_command: '&cPara utilizar este comando é necessário estar logado!' - denied_chat: '&cPara utilizar o chat é necessário estar logado!' - not_logged_in: '&cVocê não está logado!' - tempban_max_logins: '&cVocê foi temporariamente banido por tentar fazer login muitas vezes.' - max_registration: '&cVocê excedeu o número máximo de registros (%reg_count/%max_acc %reg_names) para o seu IP!' - no_permission: '&4Você não tem permissão para executar esta ação!' + unregistered_user: '&cEste usuário não está registrado!' + denied_command: '&cPara utilizar este comando é necessário estar logado!' + denied_chat: '&cPara utilizar o chat é necessário estar logado!' + not_logged_in: '&cVocê não está logado!' + tempban_max_logins: '&cVocê foi temporariamente banido por tentar fazer login muitas vezes.' + max_registration: '&cVocê excedeu o número máximo de registros (%reg_count/%max_acc %reg_names) para o seu IP!' + no_permission: '&4Você não tem permissão para executar esta ação!' unexpected_error: '&4Ocorreu um erro inesperado. Por favor contate um administrador!' kick_for_vip: '&3Um jogador VIP juntou-se ao servidor enquanto ele estava cheio!' - logged_in: '&cVocê já está logado!' - kick_unresolved_hostname: '&cErro: hostname do jogador não resolvido!' + logged_in: '&cVocê já está logado!' + kick_unresolved_hostname: '&cErro: hostname do jogador não resolvido!' # AntiBot antibot: - kick_antibot: 'O modo de proteção AntiBot está ativo, espere alguns minutos antes de entrar no servidor!' - auto_enabled: '&4O AntiBot foi ativado devido ao grande número de conexões!' - auto_disabled: '&2AntiBot desativado após %m minutos!' + kick_antibot: 'O modo de proteção AntiBot está ativo, espere alguns minutos antes de entrar no servidor!' + auto_enabled: '&4O AntiBot foi ativado devido ao grande número de conexões!' + auto_disabled: '&2AntiBot desativado após %m minutos!' # Deletar conta unregister: @@ -59,94 +59,94 @@ unregister: # Outras mensagens misc: - accounts_owned_self: 'Você tem %count contas:' + accounts_owned_self: 'Você tem %count contas:' accounts_owned_other: 'O jogador %name tem %count contas:' - account_not_activated: '&cA sua conta ainda não está ativada. Por favor, verifique seus e-mails!' + account_not_activated: '&cA sua conta ainda não está ativada. Por favor, verifique seus e-mails!' password_changed: '&2Senha alterada com sucesso!' logout: '&2Desconectado com sucesso!' - reload: '&2A configuração e o banco de dados foram recarregados corretamente!' + reload: '&2A configuração e o banco de dados foram recarregados corretamente!' usage_change_password: '&cUse: /changepassword ' -# Mensagens de sessão +# Mensagens de sessão session: - valid_session: '&2Você deslogou recentemente, então sua sessão foi retomada!' - invalid_session: '&fO seu IP foi alterado e sua sessão expirou!' + valid_session: '&2Você deslogou recentemente, então sua sessão foi retomada!' + invalid_session: '&fO seu IP foi alterado e sua sessão expirou!' # Mensagens de erro ao entrar on_join_validation: - name_length: '&4Seu nome de usuário ou é muito curto ou é muito longo!' - characters_in_name: '&4Seu nome de usuário contém caracteres inválidos. Caracteres permitidos: %valid_chars' - country_banned: '&4O seu país está banido deste servidor!' - not_owner_error: 'Você não é o proprietário da conta. Por favor, escolha outro nome!' - kick_full_server: '&4O servidor está cheio, tente novamente mais tarde!' - same_nick_online: '&4Alguém com o mesmo nome de usuário já está jogando no servidor!' - invalid_name_case: 'Você deve entrar usando o nome de usuário %valid, não %invalid.' - same_ip_online: 'Um jogador com o mesmo IP já está no servidor!' - quick_command: 'Você usou o comando muito rápido! Por favor, entre no servidor e aguarde antes de usar um comando novamente.' + name_length: '&4Seu nome de usuário ou é muito curto ou é muito longo!' + characters_in_name: '&4Seu nome de usuário contém caracteres inválidos. Caracteres permitidos: %valid_chars' + country_banned: '&4O seu país está banido deste servidor!' + not_owner_error: 'Você não é o proprietário da conta. Por favor, escolha outro nome!' + kick_full_server: '&4O servidor está cheio, tente novamente mais tarde!' + same_nick_online: '&4Alguém com o mesmo nome de usuário já está jogando no servidor!' + invalid_name_case: 'Você deve entrar usando o nome de usuário %valid, não %invalid.' + same_ip_online: 'Um jogador com o mesmo IP já está no servidor!' + quick_command: 'Você usou o comando muito rápido! Por favor, entre no servidor e aguarde antes de usar um comando novamente.' # E-mail email: usage_email_add: '&cUse: /email add ' usage_email_change: '&cUse: /email change ' - new_email_invalid: '&cE-mail novo inválido, tente novamente!' - old_email_invalid: '&cE-mail antigo inválido, tente novamente!' - invalid: '&E-mail inválido, tente novamente!' + new_email_invalid: '&cE-mail novo inválido, tente novamente!' + old_email_invalid: '&cE-mail antigo inválido, tente novamente!' + invalid: '&E-mail inválido, tente novamente!' added: '&2E-mail adicionado com sucesso!' - request_confirmation: '&cPor favor, confirme o seu endereço de e-mail!' + request_confirmation: '&cPor favor, confirme o seu endereço de e-mail!' changed: '&2E-mail alterado com sucesso!' - email_show: '&2O seu endereço de e-mail atual é: &f%email' - incomplete_settings: 'Erro: Nem todas as configurações necessárias estão definidas para o envio de e-mails. Entre em contato com um administrador.' - already_used: '&4O endereço de e-mail já está sendo usado' - send_failure: '&cO e-mail não pôde ser enviado, reporte isso a um administrador!' - no_email_for_account: '&2Você atualmente não têm endereço de e-mail associado a esta conta.' + email_show: '&2O seu endereço de e-mail atual é: &f%email' + incomplete_settings: 'Erro: Nem todas as configurações necessárias estão definidas para o envio de e-mails. Entre em contato com um administrador.' + already_used: '&4O endereço de e-mail já está sendo usado' + send_failure: '&cO e-mail não pôde ser enviado, reporte isso a um administrador!' + no_email_for_account: '&2Você atualmente não têm endereço de e-mail associado a esta conta.' add_email_request: '&3Por favor, adicione seu e-mail para a sua conta com o comando "/email add "' - change_password_expired: 'Você não pode mais usar esse comando de recuperação de senha!' - email_cooldown_error: '&cUm e-mail já foi enviado, espere %time antes de enviar novamente!' - add_not_allowed: '&cAdicionar um e-mail não é permitido.' - change_not_allowed: '&cAlterar o e-mail não é permitido.' + change_password_expired: 'Você não pode mais usar esse comando de recuperação de senha!' + email_cooldown_error: '&cUm e-mail já foi enviado, espere %time antes de enviar novamente!' + add_not_allowed: '&cAdicionar um e-mail não é permitido.' + change_not_allowed: '&cAlterar o e-mail não é permitido.' -# Recuperação de senha por e-mail +# Recuperação de senha por e-mail recovery: forgot_password_hint: '&3Esqueceu a sua senha? Use o comando "/email recovery "' command_usage: '&cUse: /email recovery ' - email_sent: '&2E-mail de recuperação enviado! Por favor, verifique sua caixa de entrada!' + email_sent: '&2E-mail de recuperação enviado! Por favor, verifique sua caixa de entrada!' code: - code_sent: 'Um código de recuperação para a redefinição de senha foi enviado ao seu e-mail.' - incorrect: 'Código de recuperação inválido! Você tem %count tentativas restantes.' - tries_exceeded: 'Você excedeu o limite de tentativas de usar o código de recuperação! Use "/email recovery [email]" para gerar um novo.' - correct: 'Código de recuperação aceito!' + code_sent: 'Um código de recuperação para a redefinição de senha foi enviado ao seu e-mail.' + incorrect: 'Código de recuperação inválido! Você tem %count tentativas restantes.' + tries_exceeded: 'Você excedeu o limite de tentativas de usar o código de recuperação! Use "/email recovery [email]" para gerar um novo.' + correct: 'Código de recuperação aceito!' change_password: 'Por favor, use o comando /email setpassword para alterar sua senha!' # Captcha captcha: - usage_captcha: '&3Para iniciar sessão você tem que resolver um captcha, utilize o comando "/captcha %captcha_code"' + usage_captcha: '&3Para iniciar sessão você tem que resolver um captcha, utilize o comando "/captcha %captcha_code"' wrong_captcha: '&cCaptcha incorreto. Por favor, escreva "/captcha %captcha_code" no chat!' valid_captcha: '&2Captcha correto!' - captcha_for_registration: 'Para se registrar você tem que resolver um código captcha, utilize o comando "/captcha %captcha_code"' - register_captcha_valid: '&2Captcha correto! Agora você pode se registrar usando /register !' + captcha_for_registration: 'Para se registrar você tem que resolver um código captcha, utilize o comando "/captcha %captcha_code"' + register_captcha_valid: '&2Captcha correto! Agora você pode se registrar usando /register !' -# Código de verificação +# Código de verificação verification: - code_required: '&3Esse comando é sensível e precisa de uma verificação via e-mail! Verifique sua caixa de entrada e siga as instruções do e-mail.' - command_usage: '&cUse: /verification ' - incorrect_code: '&cCódigo incorreto, utilize "/verification " com o código que você recebeu por e-mail!' - success: '&2Sua identidade foi verificada, agora você pode usar todos os comandos durante esta sessão.' - already_verified: '&2Você já pode executar comandos sensíveis durante esta sessão!' - code_expired: '&3O seu código expirou! Execute outro comando sensível para gerar um outro código.' - email_needed: '&3Para verificar sua identidade, você precisa vincular um e-mail à sua conta!' - -# Verificação em duas etapas + code_required: '&3Esse comando é sensível e precisa de uma verificação via e-mail! Verifique sua caixa de entrada e siga as instruções do e-mail.' + command_usage: '&cUse: /verification ' + incorrect_code: '&cCódigo incorreto, utilize "/verification " com o código que você recebeu por e-mail!' + success: '&2Sua identidade foi verificada, agora você pode usar todos os comandos durante esta sessão.' + already_verified: '&2Você já pode executar comandos sensíveis durante esta sessão!' + code_expired: '&3O seu código expirou! Execute outro comando sensível para gerar um outro código.' + email_needed: '&3Para verificar sua identidade, você precisa vincular um e-mail à sua conta!' + +# Verificação em duas etapas two_factor: - code_created: '&2O seu código secreto é %code. Você pode verificá-lo aqui %url' - confirmation_required: 'Confirme seu código com /2fa confirm ' - code_required: 'Registre o seu código de verificação em duas etapas com /2fa code ' - already_enabled: 'A verificação em duas etapas já está ativada nesta conta!' - enable_error_no_code: 'Nenhuma chave de verificação foi gerada ou ela expirou. Por favor, use /2fa add' - enable_success: 'Verificação em duas etapas ativada com sucesso para esta conta!' - enable_error_wrong_code: 'Código incorreto ou expirado! Por favor, use /2fa add' - not_enabled_error: 'A verificação em duas etapas não está ativada nesta conta. Use /2fa add' - removed_success: 'Verificação em duas etapas desativada com sucesso!' - invalid_code: 'Código inválido!' + code_created: '&2O seu código secreto é %code. Você pode verificá-lo aqui %url' + confirmation_required: 'Confirme seu código com /2fa confirm ' + code_required: 'Registre o seu código de verificação em duas etapas com /2fa code ' + already_enabled: 'A verificação em duas etapas já está ativada nesta conta!' + enable_error_no_code: 'Nenhuma chave de verificação foi gerada ou ela expirou. Por favor, use /2fa add' + enable_success: 'Verificação em duas etapas ativada com sucesso para esta conta!' + enable_error_wrong_code: 'Código incorreto ou expirado! Por favor, use /2fa add' + not_enabled_error: 'A verificação em duas etapas não está ativada nesta conta. Use /2fa add' + removed_success: 'Verificação em duas etapas desativada com sucesso!' + invalid_code: 'Código inválido!' # Unidades de tempo @@ -164,8 +164,8 @@ dialog: confirm_email: '&fConfirmar e-mail' button: '&aRegistrar' two_factor: - title: '&6Autenticação de dois fatores' - code: '&fCódigo 2FA' + title: '&6Autenticação de dois fatores' + code: '&fCódigo 2FA' button: '&aVerificar' button: cancel: '&cCancelar' @@ -180,14 +180,14 @@ time: day: 'dia' days: 'dias' -# Mensagens de validação de comandos +# Mensagens de validação de comandos command: - player_only: '&cEste comando é apenas para jogadores.' + player_only: '&cEste comando é apenas para jogadores.' player_only_alternative: '&cApenas para jogadores! Por favor, use %alternative em vez disso.' failed_to_parse: '&4Falha ao interpretar o comando do AuthMe!' unknown: '&4Comando desconhecido!' incorrect_arguments: '&4Argumentos de comando incorretos!' - did_you_mean: '&eVocê quis dizer &6%cmd&e?' + did_you_mean: '&eVocê quis dizer &6%cmd&e?' see_help: '&eUse o comando &6/%cmd&e para ver a ajuda.' detailed_help: '&6Ajuda detalhada: &f/%cmd' @@ -195,20 +195,20 @@ command: admin: force_login: player_offline: '&cO jogador precisa estar online!' - forbidden: '&cVocê não pode forçar o login do jogador %name!' - success: '&2Login forçado para %name realizado!' + forbidden: '&cVocê não pode forçar o login do jogador %name!' + success: '&2Login forçado para %name realizado!' accounts: - ip_not_found: '&cEsse IP não existe no banco de dados.' - single_account: '&2%name é um jogador com conta única.' - no_last_ip: '&cNenhum último endereço IP conhecido para o jogador.' + ip_not_found: '&cEsse IP não existe no banco de dados.' + single_account: '&2%name é um jogador com conta única.' + no_last_ip: '&cNenhum último endereço IP conhecido para o jogador.' email_show: '&2E-mail de %name: %email' antibot: status: '&2Status do AntiBot: %status' - override_enabled: '&2Substituição Manual do AntiBot: habilitada!' - override_disabled: '&2Substituição Manual do AntiBot: desabilitada!' - invalid_mode: '&cModo de AntiBot inválido!' + override_enabled: '&2Substituição Manual do AntiBot: habilitada!' + override_disabled: '&2Substituição Manual do AntiBot: desabilitada!' + invalid_mode: '&cModo de AntiBot inválido!' reload: - db_type_change: '&eNota: não é possível alterar o tipo de banco de dados durante /authme reload.' + db_type_change: '&eNota: não é possível alterar o tipo de banco de dados durante /authme reload.' error: '&cOcorreu um erro durante o recarregamento do AuthMe.' spawn: set_success: '&2Novo ponto de spawn definido corretamente.' @@ -217,3 +217,25 @@ admin: first_set_fail: '&cSetFirstSpawn falhou, por favor tente novamente.' not_defined: '&cSpawn falhou, por favor tente definir o spawn.' first_not_defined: '&cPrimeiro spawn falhou, por favor tente definir o primeiro spawn.' + +# Modo premium +premium: + feature_disabled: '&cO modo premium não está ativado neste servidor.' + account_not_found: '&cNenhuma conta Minecraft premium encontrada para o seu nome de usuário.' + already_enabled: '&eO modo premium já está ativado para a sua conta.' + enable_success: '&2Modo premium ativado! Você não precisará mais se autenticar ao entrar.' + not_enabled: '&eO modo premium não está ativado para a sua conta.' + disable_success: '&2Modo premium desativado. Você precisará se autenticar novamente.' + error: '&cOcorreu um erro ao verificar o seu status premium. Por favor, tente novamente mais tarde.' + admin: + not_registered: '&c%name não está registrado.' + already_enabled: '&eO modo premium já está ativado para %name.' + account_not_found: '&cNenhuma conta Mojang encontrada para %name.' + enable_success: '&2Modo premium ativado para %name.' + not_enabled: '&eO modo premium não está ativado para %name.' + disable_success: '&2Modo premium desativado para %name.' + impostor_kicked: '&eUm jogador conectado como %name tinha um UUID diferente e foi expulso.' + kick_reason: '&cAs configurações premium da sua conta foram alteradas por um administrador. Por favor, reconecte-se.' + pending: '&eVerificação premium pendente para %name. Eles precisam se reconectar para confirmar a propriedade da conta Mojang.' + pending_kick: '&eVerificação premium solicitada. Por favor, reconecte-se para confirmar a propriedade da sua conta Mojang.' + pending_fail: '&cA verificação premium falhou. Por favor, faça login com sua senha.' diff --git a/authme-core/src/main/resources/messages/messages_cz.yml b/authme-core/src/main/resources/messages/messages_cz.yml index 93231b2e7..4bfb02ffe 100644 --- a/authme-core/src/main/resources/messages/messages_cz.yml +++ b/authme-core/src/main/resources/messages/messages_cz.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn selhal, prosím zkus to znovu.' not_defined: '&cSpawn selhal, prosím zkus definovat spawn.' first_not_defined: '&cPrvní spawn selhal, prosím zkus definovat první spawn.' + +# Prémiový režim +premium: + feature_disabled: '&cPrémiový režim není na tomto serveru povolen.' + account_not_found: '&cPro toto uživatelské jméno nebyl nalezen žádný prémiový účet Minecraft.' + already_enabled: '&ePrémiový režim je pro váš účet již povolen.' + enable_success: '&2Prémiový režim byl aktivován! Při přihlašování se již nebudete muset ověřovat.' + not_enabled: '&ePrémiový režim není pro váš účet povolen.' + disable_success: '&2Prémiový režim byl deaktivován. Budete se muset znovu ověřit.' + error: '&cPři ověřování vašeho prémiového stavu došlo k chybě. Zkuste to prosím znovu.' + admin: + not_registered: '&c%name není zaregistrován.' + already_enabled: '&ePrémiový režim je pro %name již povolen.' + account_not_found: '&cPro %name nebyl nalezen žádný účet Mojang.' + enable_success: '&2Prémiový režim byl aktivován pro %name.' + not_enabled: '&ePrémiový režim není pro %name povolen.' + disable_success: '&2Prémiový režim byl deaktivován pro %name.' + impostor_kicked: '&eHráč připojený jako %name měl neshodující se UUID a byl vyhozen.' + kick_reason: '&cNastavení premium vašeho účtu bylo změněno administrátorem. Připojte se znovu.' + pending: '&ePrémiové ověření čeká na vyřízení pro %name. Musí se znovu připojit, aby potvrdili vlastnictví účtu Mojang.' + pending_kick: '&ePrémiové ověření bylo vyžádáno. Připojte se prosím znovu, abyste potvrdili vlastnictví svého účtu Mojang.' + pending_fail: '&cPrémiové ověření se nezdařilo. Přihlaste se prosím pomocí hesla.' diff --git a/authme-core/src/main/resources/messages/messages_de.yml b/authme-core/src/main/resources/messages/messages_de.yml index ae6ca970a..9109749b1 100644 --- a/authme-core/src/main/resources/messages/messages_de.yml +++ b/authme-core/src/main/resources/messages/messages_de.yml @@ -1,4 +1,4 @@ -# List of global tags: +# List of global tags: # %nl% - Goes to new line. # %username% - Replaces the username of the player receiving the message. # %displayname% - Replaces the nickname (and colors) of the player receiving the message. @@ -7,18 +7,18 @@ registration: disabled: '&cRegistrierungen sind deaktiviert' name_taken: '&cDieser Benutzername ist schon vergeben' - register_request: '&3Bitte registriere dich mit "/register "' - command_usage: '&cBenutze: /register ' - reg_only: '&4Nur für registrierte Spieler! Bitte besuche http://example.com um dich zu registrieren.' + register_request: '&3Bitte registriere dich mit "/register "' + command_usage: '&cBenutze: /register ' + reg_only: '&4Nur für registrierte Spieler! Bitte besuche http://example.com um dich zu registrieren.' success: '&2Erfolgreich registriert!' kicked_admin_registered: 'Ein Administrator hat dich bereits registriert; bitte logge dich erneut ein.' # Password errors on registration password: - match_error: '&cPasswörter stimmen nicht überein!' + match_error: '&cPasswörter stimmen nicht überein!' name_in_password: '&cDu kannst deinen Namen nicht als Passwort verwenden!' - unsafe_password: '&cPasswort unsicher! Bitte wähle ein anderes.' - forbidden_characters: '&4Dein Passwort enthält unerlaubte Zeichen. Zulässige Zeichen: %valid_chars' + unsafe_password: '&cPasswort unsicher! Bitte wähle ein anderes.' + forbidden_characters: '&4Dein Passwort enthält unerlaubte Zeichen. Zulässige Zeichen: %valid_chars' wrong_length: '&cDein Passwort ist zu kurz oder zu lang!' # Login @@ -27,21 +27,21 @@ login: wrong_password: '&cFalsches Passwort' success: '&2Successful login!' login_request: '&cBitte logge dich ein mit "/login "' - timeout_error: '&4Zeitüberschreitung beim Login' + timeout_error: '&4Zeitüberschreitung beim Login' # Errors error: denied_command: '&cUm diesen Befehl zu nutzen musst du authentifiziert sein!' - denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' + denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' unregistered_user: '&cBenutzername nicht registriert!' not_logged_in: '&cNicht eingeloggt!' - no_permission: '&4Du hast keine Rechte, um diese Aktion auszuführen!' + no_permission: '&4Du hast keine Rechte, um diese Aktion auszuführen!' unexpected_error: '&4Ein Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.' max_registration: '&cDu hast die maximale Anzahl an Accounts erreicht (%reg_count/%max_acc %reg_names).' logged_in: '&cBereits eingeloggt!' kick_for_vip: '&3Ein VIP-Spieler hat den vollen Server betreten!' # TODO kick_unresolved_hostname: '&cAn error occurred: unresolved player hostname!' - tempban_max_logins: '&cDu bist wegen zu vielen fehlgeschlagenen Login-Versuchen temporär gebannt!' + tempban_max_logins: '&cDu bist wegen zu vielen fehlgeschlagenen Login-Versuchen temporär gebannt!' # AntiBot antibot: @@ -51,13 +51,13 @@ antibot: # Unregister unregister: - success: '&cBenutzerkonto erfolgreich gelöscht!' + success: '&cBenutzerkonto erfolgreich gelöscht!' command_usage: '&cBenutze: /unregister ' # Other messages misc: - account_not_activated: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' - password_changed: '&2Passwort geändert!' + account_not_activated: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' + password_changed: '&2Passwort geändert!' logout: '&2Erfolgreich ausgeloggt' reload: '&2Konfiguration und Datenbank wurden erfolgreich neu geladen.' usage_change_password: '&cBenutze: /changepassword ' @@ -67,70 +67,70 @@ misc: # Session messages session: valid_session: '&2Erfolgreich eingeloggt!' - invalid_session: '&cUngültige Session. Bitte starte das Spiel neu oder warte, bis die Session abgelaufen ist.' + invalid_session: '&cUngültige Session. Bitte starte das Spiel neu oder warte, bis die Session abgelaufen ist.' # Error messages when joining on_join_validation: same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' same_nick_online: '&4Jemand mit diesem Namen spielt bereits auf dem Server!' name_length: '&4Dein Nickname ist zu kurz oder zu lang.' - characters_in_name: '&4Dein Nickname enthält unerlaubte Zeichen. Zulässige Zeichen: %valid_chars' + characters_in_name: '&4Dein Nickname enthält unerlaubte Zeichen. Zulässige Zeichen: %valid_chars' kick_full_server: '&4Der Server ist momentan voll, Sorry!' country_banned: '&4Dein Land ist gesperrt!' - not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen anderen Namen!' + not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen anderen Namen!' invalid_name_case: 'Dein registrierter Benutzername ist &2%valid&f - nicht &4%invalid&f.' quick_command: 'Du hast einen Befehl zu schnell benutzt! Bitte trete dem Server erneut bei und warte, bevor du irgendeinen Befehl nutzt.' # Email email: - add_email_request: '&3Bitte hinterlege deine E-Mail-Adresse: /email add ' - usage_email_add: '&cBenutze: /email add ' + add_email_request: '&3Bitte hinterlege deine E-Mail-Adresse: /email add ' + usage_email_add: '&cBenutze: /email add ' usage_email_change: '&cBenutze: /email change ' - new_email_invalid: '&cDie neue E-Mail ist ungültig!' - old_email_invalid: '&cDie alte E-Mail ist ungültig!' - invalid: '&cUngültige E-Mail!' - added: '&2E-Mail hinzugefügt!' - add_not_allowed: '&cHinzufügen einer E-Mail nicht gestattet!' - request_confirmation: '&cBitte bestätige deine E-Mail!' + new_email_invalid: '&cDie neue E-Mail ist ungültig!' + old_email_invalid: '&cDie alte E-Mail ist ungültig!' + invalid: '&cUngültige E-Mail!' + added: '&2E-Mail hinzugefügt!' + add_not_allowed: '&cHinzufügen einer E-Mail nicht gestattet!' + request_confirmation: '&cBitte bestätige deine E-Mail!' changed: '&2E-Mail aktualisiert!' change_not_allowed: '&cBearbeiten einer E-Mail nicht gestattet!' email_show: '&2Deine aktuelle E-Mail-Adresse ist: &f%email' - no_email_for_account: '&2Du hast zur Zeit keine E-Mail-Adresse für deinen Account hinterlegt.' + no_email_for_account: '&2Du hast zur Zeit keine E-Mail-Adresse für deinen Account hinterlegt.' already_used: '&4Diese E-Mail-Adresse wird bereits genutzt.' incomplete_settings: 'Fehler: Es wurden nicht alle notwendigen Einstellungen vorgenommen, um E-Mails zu senden. Bitte kontaktiere einen Administrator.' send_failure: 'Die E-Mail konnte nicht gesendet werden. Bitte kontaktiere einen Administrator.' - change_password_expired: 'Mit diesem Befehl kannst du dein Passwort nicht mehr ändern.' - email_cooldown_error: '&cEine E-Mail wurde erst kürzlich versendet. Du musst %time warten, bevor du eine neue anfordern kannst.' + change_password_expired: 'Mit diesem Befehl kannst du dein Passwort nicht mehr ändern.' + email_cooldown_error: '&cEine E-Mail wurde erst kürzlich versendet. Du musst %time warten, bevor du eine neue anfordern kannst.' # Password recovery by email recovery: - forgot_password_hint: '&3Passwort vergessen? Nutze "/email recovery " für ein neues Passwort' + forgot_password_hint: '&3Passwort vergessen? Nutze "/email recovery " für ein neues Passwort' command_usage: '&cBenutze: /email recovery ' email_sent: '&2Wiederherstellungs-E-Mail wurde gesendet!' code: - code_sent: 'Ein Wiederherstellungscode zum Zurücksetzen deines Passworts wurde an deine E-Mail-Adresse geschickt.' + code_sent: 'Ein Wiederherstellungscode zum Zurücksetzen deines Passworts wurde an deine E-Mail-Adresse geschickt.' incorrect: 'Der Wiederherstellungscode stimmt nicht! Du hast noch %count Versuche. Nutze /email recovery [email] um einen neuen zu generieren.' - tries_exceeded: 'Du hast die maximale Anzahl an Versuchen zur Eingabe des Wiederherstellungscodes überschritten. Benutze "/email recovery [email]" um einen neuen zu generieren.' + tries_exceeded: 'Du hast die maximale Anzahl an Versuchen zur Eingabe des Wiederherstellungscodes überschritten. Benutze "/email recovery [email]" um einen neuen zu generieren.' correct: 'Der eingegebene Wiederherstellungscode ist richtig!' - change_password: 'Benutze bitte den Befehl /email setpassword um dein Passwort umgehend zu ändern.' + change_password: 'Benutze bitte den Befehl /email setpassword um dein Passwort umgehend zu ändern.' # Captcha captcha: usage_captcha: '&3Um dich einzuloggen, tippe dieses Captcha so ein: /captcha %captcha_code' wrong_captcha: '&cFalsches Captcha, bitte nutze: /captcha %captcha_code' valid_captcha: '&2Das Captcha ist korrekt!' - captcha_for_registration: 'Um dich zu registrieren, musst du erst ein Captcha lösen, bitte nutze den Befehl: /captcha %captcha_code' + captcha_for_registration: 'Um dich zu registrieren, musst du erst ein Captcha lösen, bitte nutze den Befehl: /captcha %captcha_code' register_captcha_valid: '&2Captcha richtig! Du kannst dich jetzt registrieren mit: /register' # Verification code verification: - code_required: '&3Dieser Befehl ist sensibel und erfordert eine E-Mail-Verifizierung! Überprüfe deinen Posteingang und folge den Anweisungen der E-Mail.' + code_required: '&3Dieser Befehl ist sensibel und erfordert eine E-Mail-Verifizierung! Überprüfe deinen Posteingang und folge den Anweisungen der E-Mail.' command_usage: '&cBenutze: /verification ' incorrect_code: '&Falscher Code, bitte gib "/verification " in den Chat ein, und verwende den Code, den Du per E-Mail erhalten hast.' - success: '&2Deine Identität wurde verifiziert! Du kannst nun alle Befehle innerhalb der aktuellen Sitzung ausführen!' - already_verified: '&2Du kannst bereits jeden sensiblen Befehl innerhalb der aktuellen Sitzung ausführen!' - code_expired: '&3Dein Code ist abgelaufen! Führe einen weiteren sensiblen Befehl aus, um einen neuen Code zu erhalten!' - email_needed: '&3Um deine Identität zu überprüfen, musst Du eine E-Mail-Adresse mit deinem Konto verknüpfen!' + success: '&2Deine Identität wurde verifiziert! Du kannst nun alle Befehle innerhalb der aktuellen Sitzung ausführen!' + already_verified: '&2Du kannst bereits jeden sensiblen Befehl innerhalb der aktuellen Sitzung ausführen!' + code_expired: '&3Dein Code ist abgelaufen! Führe einen weiteren sensiblen Befehl aus, um einen neuen Code zu erhalten!' + email_needed: '&3Um deine Identität zu überprüfen, musst Du eine E-Mail-Adresse mit deinem Konto verknüpfen!' # Time units @@ -143,9 +143,9 @@ dialog: register: title: '&6Registrierung' password: '&fPasswort' - confirm_password: '&fPasswort bestätigen' + confirm_password: '&fPasswort bestätigen' email: '&fE-Mail' - confirm_email: '&fE-Mail bestätigen' + confirm_email: '&fE-Mail bestätigen' button: '&aRegistrieren' two_factor: title: '&6Zwei-Faktor-Authentifizierung' @@ -167,20 +167,20 @@ time: # Two-factor authentication two_factor: code_created: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' - confirmation_required: 'Bitte bestätige deinen Code mit /2fa confirm ' - code_required: 'Bitte übermittle deinen Zwei-Faktor-Authentifizierungscode mit /2fa code ' - already_enabled: 'Die Zwei-Faktor-Authentifizierung ist für dein Konto bereits aktiviert!' - enable_error_no_code: 'Es wurde kein 2FA-Schlüssel für dich generiert oder er ist abgelaufen. Bitte nutze /2fa add' - enable_success: 'Zwei-Faktor-Authentifizierung für dein Konto erfolgreich aktiviert' + confirmation_required: 'Bitte bestätige deinen Code mit /2fa confirm ' + code_required: 'Bitte übermittle deinen Zwei-Faktor-Authentifizierungscode mit /2fa code ' + already_enabled: 'Die Zwei-Faktor-Authentifizierung ist für dein Konto bereits aktiviert!' + enable_error_no_code: 'Es wurde kein 2FA-Schlüssel für dich generiert oder er ist abgelaufen. Bitte nutze /2fa add' + enable_success: 'Zwei-Faktor-Authentifizierung für dein Konto erfolgreich aktiviert' enable_error_wrong_code: 'Code falsch oder abgelaufen. Bitte nutze /2fa add' - not_enabled_error: 'Die Zwei-Faktor-Authentifizierung ist für dein Konto nicht aktiviert. Benutze /2fa add' + not_enabled_error: 'Die Zwei-Faktor-Authentifizierung ist für dein Konto nicht aktiviert. Benutze /2fa add' removed_success: 'Die Zwei-Faktor-Authentifizierung wurde erfolgreich von deinem Konto entfernt' - invalid_code: 'Ungültiger Code!' + invalid_code: 'Ungültiger Code!' # Command validation messages command: - player_only: '&cDieser Befehl ist nur für Spieler.' - player_only_alternative: '&cNur für Spieler! Bitte benutze stattdessen %alternative.' + player_only: '&cDieser Befehl ist nur für Spieler.' + player_only_alternative: '&cNur für Spieler! Bitte benutze stattdessen %alternative.' failed_to_parse: '&4AuthMe-Befehl konnte nicht verarbeitet werden!' unknown: '&4Unbekannter Befehl!' incorrect_arguments: '&4Falsche Befehlsargumente!' @@ -193,19 +193,19 @@ admin: force_login: player_offline: '&cDer Spieler muss online sein!' forbidden: '&cDu kannst den Spieler %name nicht zwangsweise einloggen!' - success: '&2Zwangs-Login für %name durchgeführt!' + success: '&2Zwangs-Login für %name durchgeführt!' accounts: ip_not_found: '&cDiese IP-Adresse existiert nicht in der Datenbank.' single_account: '&2%name hat nur einen Account.' - no_last_ip: '&cKeine bekannte letzte IP-Adresse für diesen Spieler.' + no_last_ip: '&cKeine bekannte letzte IP-Adresse für diesen Spieler.' email_show: '&2E-Mail von %name: %email' antibot: status: '&2AntiBot-Status: %status' override_enabled: '&2AntiBot Manueller Override: aktiviert!' override_disabled: '&2AntiBot Manueller Override: deaktiviert!' - invalid_mode: '&cUngültiger AntiBot-Modus!' + invalid_mode: '&cUngültiger AntiBot-Modus!' reload: - db_type_change: '&eHinweis: Der Datenbanktyp kann während /authme reload nicht geändert werden.' + db_type_change: '&eHinweis: Der Datenbanktyp kann während /authme reload nicht geändert werden.' error: '&cFehler beim Neuladen von AuthMe.' spawn: set_success: '&2Neuer Spawnpunkt erfolgreich gesetzt.' @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn ist fehlgeschlagen, bitte erneut versuchen.' not_defined: '&cSpawn fehlgeschlagen, bitte versuche den Spawn zu definieren.' first_not_defined: '&cErster Spawn fehlgeschlagen, bitte versuche den ersten Spawn zu definieren.' + +# Premium-Modus +premium: + feature_disabled: '&cDer Premium-Modus ist auf diesem Server nicht aktiviert.' + account_not_found: '&cKein Premium-Minecraft-Konto für deinen Benutzernamen gefunden.' + already_enabled: '&eDer Premium-Modus ist für dein Konto bereits aktiviert.' + enable_success: '&2Premium-Modus aktiviert! Du musst dich beim Login nicht mehr authentifizieren.' + not_enabled: '&eDer Premium-Modus ist für dein Konto nicht aktiviert.' + disable_success: '&2Premium-Modus deaktiviert. Du musst dich wieder authentifizieren.' + error: '&cBei der Überprüfung deines Premium-Status ist ein Fehler aufgetreten. Bitte versuche es später erneut.' + admin: + not_registered: '&c%name ist nicht registriert.' + already_enabled: '&eDer Premium-Modus ist für %name bereits aktiviert.' + account_not_found: '&cKein Mojang-Konto für %name gefunden.' + enable_success: '&2Premium-Modus für %name aktiviert.' + not_enabled: '&eDer Premium-Modus ist für %name nicht aktiviert.' + disable_success: '&2Premium-Modus für %name deaktiviert.' + impostor_kicked: '&eEin Spieler, der als %name verbunden war, hatte eine abweichende UUID und wurde gekickt.' + kick_reason: '&cDie Premium-Einstellungen deines Kontos wurden von einem Admin geändert. Bitte verbinde dich erneut.' + pending: '&ePremium-Verifizierung ausstehend für %name. Sie müssen sich erneut verbinden, um den Besitz des Mojang-Kontos zu bestätigen.' + pending_kick: '&ePremium-Verifizierung angefordert. Bitte verbinde dich erneut, um den Besitz deines Mojang-Kontos zu bestätigen.' + pending_fail: '&cPremium-Verifizierung fehlgeschlagen. Bitte melde dich mit deinem Passwort an.' diff --git a/authme-core/src/main/resources/messages/messages_en.yml b/authme-core/src/main/resources/messages/messages_en.yml index e4810725d..001f78760 100644 --- a/authme-core/src/main/resources/messages/messages_en.yml +++ b/authme-core/src/main/resources/messages/messages_en.yml @@ -212,3 +212,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Premium mode +premium: + feature_disabled: '&cPremium mode is not enabled on this server.' + account_not_found: '&cNo Mojang account found for your username.' + already_enabled: '&ePremium mode is already enabled for your account.' + enable_success: '&2Premium mode enabled! You will no longer need to authenticate on login.' + not_enabled: '&ePremium mode is not enabled for your account.' + disable_success: '&2Premium mode disabled. You will need to authenticate again.' + error: '&cAn error occurred while verifying your premium status. Please try again later.' + admin: + not_registered: '&c%name is not registered.' + already_enabled: '&ePremium mode is already enabled for %name.' + account_not_found: '&cNo Mojang account found for %name.' + enable_success: '&2Premium mode enabled for %name.' + not_enabled: '&ePremium mode is not enabled for %name.' + disable_success: '&2Premium mode disabled for %name.' + impostor_kicked: '&eA player online as %name had a mismatched UUID and has been kicked.' + kick_reason: '&cPremium settings for your account have been changed by an admin. Please reconnect.' + pending: '&ePremium verification pending for %name. They must reconnect to confirm ownership of the Mojang account.' + pending_kick: '&ePremium verification requested. Please reconnect to confirm ownership of your Mojang account.' + pending_fail: '&cPremium verification failed. Please log in with your password.' diff --git a/authme-core/src/main/resources/messages/messages_eo.yml b/authme-core/src/main/resources/messages/messages_eo.yml index 5c7b1c0ad..2cd7868ef 100644 --- a/authme-core/src/main/resources/messages/messages_eo.yml +++ b/authme-core/src/main/resources/messages/messages_eo.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Superpaga reĝimo +premium: + feature_disabled: '&cSuperpaga reĝimo ne estas aktivigita sur ĉi tiu servilo.' + account_not_found: '&cNeniu superpaga Minecraft-konto trovita por via uzantnomo.' + already_enabled: '&eSuperpaga reĝimo jam estas aktivigita por via konto.' + enable_success: '&2Superpaga reĝimo aktivigita! Vi ne plu bezonos aŭtentikigi dum ensaluto.' + not_enabled: '&eSuperpaga reĝimo ne estas aktivigita por via konto.' + disable_success: '&2Superpaga reĝimo malaktivigita. Vi devos denove aŭtentikigi.' + error: '&cEraro okazis dum kontrolado de via superpaga stato. Bonvolu reprovi poste.' + admin: + not_registered: '&c%name ne estas registrita.' + already_enabled: '&eSuperpaga reĝimo jam estas aktivigita por %name.' + account_not_found: '&cNeniu Mojang-konto trovita por %name.' + enable_success: '&2Superpaga reĝimo aktivigita por %name.' + not_enabled: '&eSuperpaga reĝimo ne estas aktivigita por %name.' + disable_success: '&2Superpaga reĝimo malaktivigita por %name.' + impostor_kicked: '&eLudanto konektita kiel %name havis malsaman UUID kaj estis forĵetita.' + kick_reason: '&cViaj superpagaj agordoj estis ŝanĝitaj de administranto. Bonvolu rekonekti.' + pending: '&eSuperpaga konfirmado estas ankoraŭ traktata por %name. Ili devas rekonekti por konfirmi proprieton de la Mojang-konto.' + pending_kick: '&eSuperpaga konfirmado estis petita. Bonvolu rekonekti por konfirmi proprieton de via Mojang-konto.' + pending_fail: '&cSuperpaga konfirmado malsukcesis. Bonvolu ensaluti per via pasvorto.' diff --git a/authme-core/src/main/resources/messages/messages_es.yml b/authme-core/src/main/resources/messages/messages_es.yml index 29fb59bce..dafcc5804 100644 --- a/authme-core/src/main/resources/messages/messages_es.yml +++ b/authme-core/src/main/resources/messages/messages_es.yml @@ -215,3 +215,25 @@ admin: first_set_fail: '&cSetFirstSpawn falló, por favor inténtalo de nuevo.' not_defined: '&cEl spawn falló, por favor intenta definir el spawn.' first_not_defined: '&cEl primer spawn falló, por favor intenta definir el primer spawn.' + +# Modo premium +premium: + feature_disabled: '&cEl modo premium no está activado en este servidor.' + account_not_found: '&cNo se encontró ninguna cuenta premium de Minecraft para tu nombre de usuario.' + already_enabled: '&eEl modo premium ya está activado para tu cuenta.' + enable_success: '&2¡Modo premium activado! Ya no necesitarás autenticarte al iniciar sesión.' + not_enabled: '&eEl modo premium no está activado para tu cuenta.' + disable_success: '&2Modo premium desactivado. Deberás autenticarte de nuevo.' + error: '&cOcurrió un error al verificar tu estado premium. Por favor, inténtalo de nuevo más tarde.' + admin: + not_registered: '&c%name no está registrado.' + already_enabled: '&eEl modo premium ya está activado para %name.' + account_not_found: '&cNo se encontró ninguna cuenta de Mojang para %name.' + enable_success: '&2Modo premium activado para %name.' + not_enabled: '&eEl modo premium no está activado para %name.' + disable_success: '&2Modo premium desactivado para %name.' + impostor_kicked: '&eUn jugador conectado como %name tenía un UUID diferente y ha sido expulsado.' + kick_reason: '&cLa configuración premium de tu cuenta ha sido modificada por un administrador. Por favor, vuelve a conectarte.' + pending: '&eVerificación premium pendiente para %name. Deben reconectarse para confirmar la propiedad de la cuenta de Mojang.' + pending_kick: '&eVerificación premium solicitada. Por favor, vuelve a conectarte para confirmar la propiedad de tu cuenta de Mojang.' + pending_fail: '&cLa verificación premium ha fallado. Por favor, inicia sesión con tu contraseña.' diff --git a/authme-core/src/main/resources/messages/messages_et.yml b/authme-core/src/main/resources/messages/messages_et.yml index 518733efa..386960003 100644 --- a/authme-core/src/main/resources/messages/messages_et.yml +++ b/authme-core/src/main/resources/messages/messages_et.yml @@ -1,53 +1,53 @@ -# List of global tags: +# List of global tags: # %nl% - Goes to new line. # %username% - Replaces the username of the player receiving the message. # %displayname% - Replaces the nickname (and colors) of the player receiving the message. # Registration registration: - register_request: '&3Palun registreeri käsklusega: /register ' + register_request: '&3Palun registreeri käsklusega: /register ' command_usage: '&cKasutus: /register ' - reg_only: '&4Vaid registreeritud mängijad saavad serveriga liituda! Enda kasutaja registreerimiseks külasta http://example.com!' + reg_only: '&4Vaid registreeritud mängijad saavad serveriga liituda! Enda kasutaja registreerimiseks külasta http://example.com!' kicked_admin_registered: 'Administraator registreeris su kasutaja, palun logi uuesti sisse.' success: '&2Edukalt registreeritud!' - disabled: '&cMängusisene registreerimine on välja lülitatud!' + disabled: '&cMängusisene registreerimine on välja lülitatud!' name_taken: '&cSee kasutaja on juba registreeritud!' # Password errors on registration password: match_error: '&cParoolid ei kattu, palun proovi uuesti!' - name_in_password: '&cSa ei saa oma kasutajanime paroolina kasutada, palun vali mõni teine parool.' - unsafe_password: '&cSee parool ei ole turvaline, palun vali mõni teine parool.' - forbidden_characters: '&4Sinu parool sisaldab keelatud tähemärke. Lubatud tähemärgid: %valid_chars' - wrong_length: '&cSinu parool on liiga pikk või lühike, palun vali mõni teine parool.' + name_in_password: '&cSa ei saa oma kasutajanime paroolina kasutada, palun vali mõni teine parool.' + unsafe_password: '&cSee parool ei ole turvaline, palun vali mõni teine parool.' + forbidden_characters: '&4Sinu parool sisaldab keelatud tähemärke. Lubatud tähemärgid: %valid_chars' + wrong_length: '&cSinu parool on liiga pikk või lühike, palun vali mõni teine parool.' # Login login: command_usage: '&cKasutus: /login ' wrong_password: '&cVale parool!' success: '&2Edukalt sisselogitud!' - login_request: '&cPalun logi sisse kasutades käsklust: /login ' - timeout_error: '&4Sisselogimiseks antud aeg on läbi ning sind on serverist välja visatud, palun proovi uuesti!' + login_request: '&cPalun logi sisse kasutades käsklust: /login ' + timeout_error: '&4Sisselogimiseks antud aeg on läbi ning sind on serverist välja visatud, palun proovi uuesti!' # Errors error: unregistered_user: '&cSee kasutaja ei ole registreeritud!' - denied_command: '&cSelle käskluse kasutamiseks pead olema sisselogitud!' + denied_command: '&cSelle käskluse kasutamiseks pead olema sisselogitud!' denied_chat: '&cVestlemiseks pead olema sisselogitud!' not_logged_in: '&cSa ei ole sisselogitud!' tempban_max_logins: '&cSind on ajutiselt serverist blokeeritud, kuna sisestasid mitu korda vale parooli.' max_registration: '&cSinu IP-aadressile on registreeritud liiga palju kasutajaid! (%reg_count/%max_acc %reg_names)' - no_permission: '&4Sul puudub selle käskluse kasutamiseks luba.' - unexpected_error: '&4Esines ootamatu tõrge, palun teavita administraatorit!' - kick_for_vip: '&3VIP-mängija liitus serveriga ajal, mil see oli täis!' + no_permission: '&4Sul puudub selle käskluse kasutamiseks luba.' + unexpected_error: '&4Esines ootamatu tõrge, palun teavita administraatorit!' + kick_for_vip: '&3VIP-mängija liitus serveriga ajal, mil see oli täis!' logged_in: '&cSa oled juba sisselogitud!' - kick_unresolved_hostname: '&cEsines tõrge: mängija hostinimi on lahendamata!' + kick_unresolved_hostname: '&cEsines tõrge: mängija hostinimi on lahendamata!' # AntiBot antibot: - kick_antibot: 'AntiBot-kaitse sisse lülitatud! Pead ootama mõne minuti enne kui serveriga liituda saad.' - auto_enabled: '&4[AntiBotTeenus] AntiBot sisselülitatud!' - auto_disabled: '&2[AntiBotTeenus] AntiBot välja lülitatud peale %m minutit!' + kick_antibot: 'AntiBot-kaitse sisse lülitatud! Pead ootama mõne minuti enne kui serveriga liituda saad.' + auto_enabled: '&4[AntiBotTeenus] AntiBot sisselülitatud!' + auto_disabled: '&2[AntiBotTeenus] AntiBot välja lülitatud peale %m minutit!' unregister: success: '&cKasutaja edukalt kustutatud!' @@ -56,29 +56,29 @@ unregister: # Other messages misc: accounts_owned_self: 'Sa omad %count kontot:' - accounts_owned_other: 'Mängijal %name on %count kontot:' + accounts_owned_other: 'Mängijal %name on %count kontot:' account_not_activated: '&cSinu konto ei ole veel aktiveeritud, kontrolli oma meili!' password_changed: '&2Parool edukalt vahetatud!' - logout: '&2Edukalt välja logitud!' + logout: '&2Edukalt välja logitud!' reload: '&2Seadistused ning andmebaas on edukalt taaslaaditud!' usage_change_password: '&cKasutus: /changepassword ' # Session messages session: invalid_session: '&cSinu IP-aadress muutus, seega sinu sessioon aegus!' - valid_session: '&2Sisse logitud sessiooni jätkumise tõttu.' + valid_session: '&2Sisse logitud sessiooni jätkumise tõttu.' # Error messages when joining on_join_validation: - name_length: '&4Sinu kasutajanimi on liiga pikk või liiga lühike!' - characters_in_name: '&4Sinu kasutajanimi sisaldab keelatud tähemärke. Lubatud tähemärgid: %valid_chars' - country_banned: '&4Sinu riigist ei ole võimalik sellesse serverisse ühenduda!' + name_length: '&4Sinu kasutajanimi on liiga pikk või liiga lühike!' + characters_in_name: '&4Sinu kasutajanimi sisaldab keelatud tähemärke. Lubatud tähemärgid: %valid_chars' + country_banned: '&4Sinu riigist ei ole võimalik sellesse serverisse ühenduda!' not_owner_error: 'Sa ei ole selle konto omanik. Vali teine nimi!' - kick_full_server: '&4Server on täis, proovi hiljem uuesti!' - same_nick_online: '&4Sama kasutaja on juba serveriga ühendatud!' + kick_full_server: '&4Server on täis, proovi hiljem uuesti!' + same_nick_online: '&4Sama kasutaja on juba serveriga ühendatud!' invalid_name_case: 'Sa peaksid liituma nimega %valid, mitte nimega %invalid.' - same_ip_online: 'Sama IP-aadressiga mängija juba mängib!' - quick_command: 'Sa kasutasid käsklust liiga kiiresti! Palun liitu serveriga uuesti ning oota enne mõne käskluse kasutamist kauem.' + same_ip_online: 'Sama IP-aadressiga mängija juba mängib!' + quick_command: 'Sa kasutasid käsklust liiga kiiresti! Palun liitu serveriga uuesti ning oota enne mõne käskluse kasutamist kauem.' # Email email: @@ -91,56 +91,56 @@ email: request_confirmation: '&cPalun kinnita oma meiliaadress!' changed: '&2Meiliaadress edukalt muudetud!' email_show: '&2Sinu praegune meiliaadress on: &f%email' - incomplete_settings: 'Viga: meili saatmiseks pole kõik vajalikud seaded seadistatud. Teata sellest administraatorit.' + incomplete_settings: 'Viga: meili saatmiseks pole kõik vajalikud seaded seadistatud. Teata sellest administraatorit.' already_used: '&4See meiliaadress on juba kasutuses!' - send_failure: 'Meili ei õnnestunud saata. Teata sellest administraatorit.' - no_email_for_account: '&2Selle kasutajaga ei ole seotud ühtegi meiliaadressi.' - add_email_request: '&3Palun seo oma kasutajaga meiliaadress kasutades käsklust: /email add ' - change_password_expired: 'Selle käsklusega ei saa sa enam parooli muuta.' - email_cooldown_error: '&cMeil on juba saadetud. Sa pead ootama %time enne kui saad küsida uue saatmist.' + send_failure: 'Meili ei õnnestunud saata. Teata sellest administraatorit.' + no_email_for_account: '&2Selle kasutajaga ei ole seotud ühtegi meiliaadressi.' + add_email_request: '&3Palun seo oma kasutajaga meiliaadress kasutades käsklust: /email add ' + change_password_expired: 'Selle käsklusega ei saa sa enam parooli muuta.' + email_cooldown_error: '&cMeil on juba saadetud. Sa pead ootama %time enne kui saad küsida uue saatmist.' add_not_allowed: '&cMeiliaadressi lisamine ei ole lubatud.' change_not_allowed: '&cMeiliaadressi muutmine ei ole lubatud.' # Password recovery by email recovery: - forgot_password_hint: '&3Unustasid oma parooli? Kasuta käsklust: /email recovery ' + forgot_password_hint: '&3Unustasid oma parooli? Kasuta käsklust: /email recovery ' command_usage: '&cKasutus: /email recovery ' email_sent: '&2Konto taastamiseks vajalik meil saadetud! Vaata oma postkasti.' code: code_sent: 'Konto taastamise kood on saadetud!' - incorrect: 'Kood on vale! Sul on %count katset jäänud.' - tries_exceeded: 'Sul on katsed otsas. Kasuta käsklust "/email recovery [email]" uue koodi saamiseks.' - correct: 'Kood õigesti sisestatud!' - change_password: 'Palun kasuta parooli muutmiseks käsklust /email setpassword .' + incorrect: 'Kood on vale! Sul on %count katset jäänud.' + tries_exceeded: 'Sul on katsed otsas. Kasuta käsklust "/email recovery [email]" uue koodi saamiseks.' + correct: 'Kood õigesti sisestatud!' + change_password: 'Palun kasuta parooli muutmiseks käsklust /email setpassword .' # Captcha captcha: - usage_captcha: '&3Sisselogimiseks lahenda robotilõks käsklusega: /captcha %captcha_code' - wrong_captcha: '&cVale robotilõks, kasuta käsklust "/captcha %captcha_code"!' - valid_captcha: '&2Robotilõks lahendatud!' - captcha_for_registration: 'Registreerimiseks lahenda robotilõks kasutades käsklust: /captcha %captcha_code' - register_captcha_valid: '&2Robotilõks lahendatud! Võid nüüd registreerida kasutades käsklust /register' + usage_captcha: '&3Sisselogimiseks lahenda robotilõks käsklusega: /captcha %captcha_code' + wrong_captcha: '&cVale robotilõks, kasuta käsklust "/captcha %captcha_code"!' + valid_captcha: '&2Robotilõks lahendatud!' + captcha_for_registration: 'Registreerimiseks lahenda robotilõks kasutades käsklust: /captcha %captcha_code' + register_captcha_valid: '&2Robotilõks lahendatud! Võid nüüd registreerida kasutades käsklust /register' # Verification code verification: - code_required: '&3See käsklus on ohtlik mistõttu saatsime sulle meili. Kontrolli oma meili ja järgi saadetud meili juhiseid.' + code_required: '&3See käsklus on ohtlik mistõttu saatsime sulle meili. Kontrolli oma meili ja järgi saadetud meili juhiseid.' command_usage: '&cKasutus: /verification ' - incorrect_code: '&cVale kood, palun kasuta käsklust "/verification " koodiga, mille saatsime sulle meilile.' - success: '&2Sinu identiteet on kinnitatud! Sa saad nüüd praeguse sessiooni jal kasutada kõiki käsklusi.' - already_verified: '&2Sa juba saad kasutada kõiki ohtlikke käsklusi!' - code_expired: '&3Kood on aegunud! Kasuta mõnda ohtlikku käsklust, et saada uus kood!' + incorrect_code: '&cVale kood, palun kasuta käsklust "/verification " koodiga, mille saatsime sulle meilile.' + success: '&2Sinu identiteet on kinnitatud! Sa saad nüüd praeguse sessiooni jal kasutada kõiki käsklusi.' + already_verified: '&2Sa juba saad kasutada kõiki ohtlikke käsklusi!' + code_expired: '&3Kood on aegunud! Kasuta mõnda ohtlikku käsklust, et saada uus kood!' email_needed: '&3Konto kinnitamiseks pead siduma oma kontoga enda meiliaadressi!' # Two-factor authentication two_factor: - code_created: '&2Sinu privaatne kood on %code. Sa saad selle skännida aadressil %url' - confirmation_required: 'Palun kinnita oma kaheastmeline autentimise kood käsklusega /2fa confirm ' + code_created: '&2Sinu privaatne kood on %code. Sa saad selle skännida aadressil %url' + confirmation_required: 'Palun kinnita oma kaheastmeline autentimise kood käsklusega /2fa confirm ' code_required: 'Palun sisesta kaheastmeline autentimise kood kasutades /2fa code ' - already_enabled: 'Kaheastmeline autentimine on juba sisselülitatud!' - enable_error_no_code: '2FA võtit ei ole genereeritud või on see aegunud. Kasuta käsklust /2fa add' - enable_success: 'Kaheastmeline autentimine edukalt sisselülitatud!' - enable_error_wrong_code: 'Vale kood või kood on aegunud. Kasuta käsklust /2fa add' - not_enabled_error: 'Kaheastmeline autentimine ei ole su kontol sisse lülitatud. Kasuta käsklust /2fa add' + already_enabled: 'Kaheastmeline autentimine on juba sisselülitatud!' + enable_error_no_code: '2FA võtit ei ole genereeritud või on see aegunud. Kasuta käsklust /2fa add' + enable_success: 'Kaheastmeline autentimine edukalt sisselülitatud!' + enable_error_wrong_code: 'Vale kood või kood on aegunud. Kasuta käsklust /2fa add' + not_enabled_error: 'Kaheastmeline autentimine ei ole su kontol sisse lülitatud. Kasuta käsklust /2fa add' removed_success: 'Sinu kontolt on edukalt eemaldatud kaheastmeline autentimine.' invalid_code: 'Vale kood!' @@ -164,7 +164,7 @@ dialog: code: '&f2FA kood' button: '&aKinnita' button: - cancel: '&cTühista' + cancel: '&cTühista' time: second: 'sekund' @@ -173,8 +173,8 @@ time: minutes: 'minutit' hour: 'tund' hours: 'tundi' - day: 'päev' - days: 'päeva' + day: 'päev' + days: 'päeva' # Command validation messages command: @@ -213,3 +213,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Premium-režiim +premium: + feature_disabled: '&cPremium-režiim ei ole sellel serveril lubatud.' + account_not_found: '&cTeie kasutajanime jaoks ei leitud ühtegi Minecrafti premium-kontot.' + already_enabled: '&ePremium-režiim on teie kontol juba lubatud.' + enable_success: '&2Premium-režiim aktiveeritud! Te ei pea enam sisselogimisel autentima.' + not_enabled: '&ePremium-režiim ei ole teie kontol lubatud.' + disable_success: '&2Premium-režiim deaktiveeritud. Peate uuesti autentima.' + error: '&cTeie premiumi oleku kontrollimisel ilmnes viga. Palun proovige hiljem uuesti.' + admin: + not_registered: '&c%name ei ole registreeritud.' + already_enabled: '&ePremium-režiim on %name jaoks juba lubatud.' + account_not_found: '&c%name jaoks ei leitud Mojang-kontot.' + enable_success: '&2Premium-režiim lubatud kasutajale %name.' + not_enabled: '&ePremium-režiim ei ole %name jaoks lubatud.' + disable_success: '&2Premium-režiim keelatud kasutajale %name.' + impostor_kicked: '&eMängija, kes oli ühendatud nimena %name, omas erinevat UUID-d ja visati välja.' + kick_reason: '&cTeie konto premium-seadeid muutis administraator. Palun ühenduge uuesti.' + pending: '&ePremium kontrollitakse %name jaoks. Nad peavad uuesti ühenduma, et kinnitada Mojang-i konto omandiõigust.' + pending_kick: '&ePremium kinnitamine nõutud. Palun ühenduge uuesti, et kinnitada oma Mojang-i konto omandiõigust.' + pending_fail: '&cPremium kinnitamine ebaõnnestus. Palun logige sisse oma parooliga.' diff --git a/authme-core/src/main/resources/messages/messages_eu.yml b/authme-core/src/main/resources/messages/messages_eu.yml index 6e3c27427..e18a230be 100644 --- a/authme-core/src/main/resources/messages/messages_eu.yml +++ b/authme-core/src/main/resources/messages/messages_eu.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Premium modua +premium: + feature_disabled: '&cPremium modua ez dago gaitu zerbitzari honetan.' + account_not_found: '&cEz da aurkitu zure erabiltzaile-izenerako Minecraft premium konturik.' + already_enabled: '&ePremium modua dagoeneko gaituta dago zure kontuan.' + enable_success: '&2Premium modua gaituta! Ez duzu gehiago autentifikatu beharko saioa hasterakoan.' + not_enabled: '&ePremium modua ez dago gaituta zure kontuan.' + disable_success: '&2Premium modua desgaitu da. Berriro autentifikatu beharko duzu.' + error: '&cErrore bat gertatu da zure premium egoera egiaztatzean. Mesedez, saiatu berriro geroago.' + admin: + not_registered: '&c%name ez dago erregistratuta.' + already_enabled: '&ePremium modua dagoeneko gaituta dago %name erabiltzailearentzat.' + account_not_found: '&cEz da aurkitu %name-rentzako Mojang konturik.' + enable_success: '&2Premium modua gaituta %name erabiltzailearentzat.' + not_enabled: '&ePremium modua ez dago gaituta %name erabiltzailearentzat.' + disable_success: '&2Premium modua desgaituta %name erabiltzailearentzat.' + impostor_kicked: '&e%name bezala konektatuta zegoen jokalari batek UUID desberdina zuen eta kanporatu da.' + kick_reason: '&cZure kontuaren premium ezarpenak administratzaileak aldatu ditu. Mesedez, berriro konektatu.' + pending: '&ePremium egiaztapena zain dago %name erabiltzailearentzat. Berr konektatu behar dute Mojang kontuaren jabetza baieztatzeko.' + pending_kick: '&ePremium egiaztapena eskatuta. Berr konektatu zaitez zure Mojang kontuaren jabetza baieztatzeko.' + pending_fail: '&cPremium egiaztapenak huts egin du. Mesedez, saioa hasi zure pasahitzarekin.' diff --git a/authme-core/src/main/resources/messages/messages_fi.yml b/authme-core/src/main/resources/messages/messages_fi.yml index 775f76319..e1840bf62 100644 --- a/authme-core/src/main/resources/messages/messages_fi.yml +++ b/authme-core/src/main/resources/messages/messages_fi.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn epäonnistui, yritä uudelleen.' not_defined: '&cSpawn epäonnistui, yritä määrittää spawnauspiste.' first_not_defined: '&cEnsimmäinen spawn epäonnistui, yritä määrittää ensimmäinen spawnauspiste.' + +# Premium-tila +premium: + feature_disabled: '&cPremium-tila ei ole käytössä tällä palvelimella.' + account_not_found: '&cKäyttäjänimellesi ei löytynyt yhtään Minecraft-premium-tiliä.' + already_enabled: '&ePremium-tila on jo käytössä tilillesi.' + enable_success: '&2Premium-tila aktivoitu! Sinun ei enää tarvitse kirjautua sisään.' + not_enabled: '&ePremium-tila ei ole käytössä tilillesi.' + disable_success: '&2Premium-tila poistettu käytöstä. Sinun täytyy kirjautua uudelleen.' + error: '&cPremium-tilasi tarkistamisessa tapahtui virhe. Yritä myöhemmin uudelleen.' + admin: + not_registered: '&c%name ei ole rekisteröitynyt.' + already_enabled: '&ePremium-tila on jo käytössä käyttäjälle %name.' + account_not_found: '&cMojang-tiliä ei löytynyt käyttäjälle %name.' + enable_success: '&2Premium-tila aktivoitu käyttäjälle %name.' + not_enabled: '&ePremium-tila ei ole käytössä käyttäjälle %name.' + disable_success: '&2Premium-tila poistettu käytöstä käyttäjälle %name.' + impostor_kicked: '&ePelaajalla, joka oli yhteydessä nimellä %name, oli eri UUID ja hänet potkittiin.' + kick_reason: '&cTilisi premium-asetuksia on muutettu ylläpitäjän toimesta. Yhdistä uudelleen.' + pending: '&ePremium-varmennus odottaa käyttäjälle %name. Heidän on yhdistettävä uudelleen vahvistaakseen Mojang-tilin omistajuuden.' + pending_kick: '&ePremium-varmennus pyydetty. Yhdistä uudelleen vahvistaaksesi Mojang-tilisi omistajuuden.' + pending_fail: '&cPremium-varmennus epäonnistui. Kirjaudu sisään salasanallasi.' diff --git a/authme-core/src/main/resources/messages/messages_fr.yml b/authme-core/src/main/resources/messages/messages_fr.yml index 327f45098..b97e2bf0a 100644 --- a/authme-core/src/main/resources/messages/messages_fr.yml +++ b/authme-core/src/main/resources/messages/messages_fr.yml @@ -217,3 +217,25 @@ admin: first_set_fail: '&cLa définition du premier spawn a échoué, veuillez réessayer.' not_defined: '&cLe spawn a échoué, veuillez essayer de définir le spawn.' first_not_defined: '&cLe premier spawn a échoué, veuillez essayer de définir le premier spawn.' + +# Mode premium +premium: + feature_disabled: '&cLe mode premium n''est pas activé sur ce serveur.' + account_not_found: '&cAucun compte Minecraft premium trouvé pour votre pseudo.' + already_enabled: '&eLe mode premium est déjà activé pour votre compte.' + enable_success: '&2Mode premium activé ! Vous n''aurez plus besoin de vous authentifier à la connexion.' + not_enabled: '&eLe mode premium n''est pas activé pour votre compte.' + disable_success: '&2Mode premium désactivé. Vous devrez à nouveau vous authentifier.' + error: '&cUne erreur s''est produite lors de la vérification de votre statut premium. Veuillez réessayer plus tard.' + admin: + not_registered: '&c%name n''est pas inscrit.' + already_enabled: '&eLe mode premium est déjà activé pour %name.' + account_not_found: '&cAucun compte Mojang trouvé pour %name.' + enable_success: '&2Mode premium activé pour %name.' + not_enabled: '&eLe mode premium n''est pas activé pour %name.' + disable_success: '&2Mode premium désactivé pour %name.' + impostor_kicked: '&eUn joueur connecté en tant que %name avait un UUID différent et a été expulsé.' + kick_reason: '&cLes paramètres premium de votre compte ont été modifiés par un administrateur. Veuillez vous reconnecter.' + pending: '&eVérification premium en attente pour %name. Il doit se reconnecter pour confirmer la propriété du compte Mojang.' + pending_kick: '&eVérification premium demandée. Veuillez vous reconnecter pour confirmer la propriété de votre compte Mojang.' + pending_fail: '&cLa vérification premium a échoué. Veuillez vous connecter avec votre mot de passe.' diff --git a/authme-core/src/main/resources/messages/messages_gl.yml b/authme-core/src/main/resources/messages/messages_gl.yml index 59a117631..7b2d298b5 100644 --- a/authme-core/src/main/resources/messages/messages_gl.yml +++ b/authme-core/src/main/resources/messages/messages_gl.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Modo premium +premium: + feature_disabled: '&cO modo premium non está activado neste servidor.' + account_not_found: '&cNon se atopou ningunha conta Minecraft premium para o teu nome de usuario.' + already_enabled: '&eO modo premium xa está activado para a túa conta.' + enable_success: '&2Modo premium activado! Xa non necesitarás autenticarte ao iniciar sesión.' + not_enabled: '&eO modo premium non está activado para a túa conta.' + disable_success: '&2Modo premium desactivado. Necesitarás autenticarte de novo.' + error: '&cOcorreu un erro ao verificar o teu estado premium. Por favor, téntao de novo máis tarde.' + admin: + not_registered: '&c%name non está rexistrado.' + already_enabled: '&eO modo premium xa está activado para %name.' + account_not_found: '&cNon se atopou ningunha conta de Mojang para %name.' + enable_success: '&2Modo premium activado para %name.' + not_enabled: '&eO modo premium non está activado para %name.' + disable_success: '&2Modo premium desactivado para %name.' + impostor_kicked: '&eUn xogador conectado como %name tiña un UUID diferente e foi expulsado.' + kick_reason: '&cA configuración premium da túa conta foi modificada por un administrador. Por favor, vólvete conectar.' + pending: '&eVerificación premium pendente para %name. Deben volver a conectarse para confirmar a propiedade da conta de Mojang.' + pending_kick: '&eVerificación premium solicitada. Por favor, vólvete conectar para confirmar a propiedade da túa conta de Mojang.' + pending_fail: '&cA verificación premium fallou. Por favor, inicia sesión co teu contrasinal.' diff --git a/authme-core/src/main/resources/messages/messages_hu.yml b/authme-core/src/main/resources/messages/messages_hu.yml index d86f41f20..0679f600a 100644 --- a/authme-core/src/main/resources/messages/messages_hu.yml +++ b/authme-core/src/main/resources/messages/messages_hu.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cA SetFirstSpawn sikertelen volt, kérjük próbáld újra.' not_defined: '&cA spawn sikertelen volt, kérjük próbáld meg meghatározni a spawnpontot.' first_not_defined: '&cAz első spawn sikertelen volt, kérjük próbáld meg meghatározni az első spawnpontot.' + +# Prémium mód +premium: + feature_disabled: '&cA prémium mód nincs engedélyezve ezen a szerveren.' + account_not_found: '&cNem található prémium Minecraft-fiók a felhasználónevedhez.' + already_enabled: '&eA prémium mód már engedélyezve van a fiókodon.' + enable_success: '&2Prémium mód engedélyezve! Többé nem kell bejelentkezéskor hitelesítened magad.' + not_enabled: '&eA prémium mód nincs engedélyezve a fiókodon.' + disable_success: '&2Prémium mód letiltva. Újra hitelesítened kell magad.' + error: '&cHiba történt a prémium státuszod ellenőrzése során. Kérjük, próbáld meg később.' + admin: + not_registered: '&c%name nincs regisztrálva.' + already_enabled: '&eA prémium mód már engedélyezve van %name számára.' + account_not_found: '&cNem található Mojang-fiók %name számára.' + enable_success: '&2Prémium mód engedélyezve %name számára.' + not_enabled: '&eA prémium mód nincs engedélyezve %name számára.' + disable_success: '&2Prémium mód letiltva %name számára.' + impostor_kicked: '&eEgy %name nevén csatlakozó játékosnak eltérő UUID-ja volt, és kirúgták.' + kick_reason: '&cA fiókod prémium beállításait egy adminisztrátor módosította. Kérjük, csatlakozz újra.' + pending: '&ePrémium ellenőrzés folyamatban %name számára. Újra kell csatlakozniuk a Mojang-fiók tulajdonjogának megerősítéséhez.' + pending_kick: '&ePrémium ellenőrzés kérve. Kérjük, csatlakozz újra a Mojang-fiókod tulajdonjogának megerősítéséhez.' + pending_fail: '&cA prémium ellenőrzés sikertelen. Kérjük, jelentkezz be a jelszavaddal.' diff --git a/authme-core/src/main/resources/messages/messages_id.yml b/authme-core/src/main/resources/messages/messages_id.yml index ec20caafd..3bb93a115 100644 --- a/authme-core/src/main/resources/messages/messages_id.yml +++ b/authme-core/src/main/resources/messages/messages_id.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn gagal, coba lagi.' not_defined: '&cSpawn gagal, coba tentukan titik spawn.' first_not_defined: '&cSpawn pertama gagal, coba tentukan titik spawn pertama.' + +# Mode premium +premium: + feature_disabled: '&cMode premium tidak diaktifkan di server ini.' + account_not_found: '&cTidak ditemukan akun Minecraft premium untuk nama pengguna Anda.' + already_enabled: '&eMode premium sudah diaktifkan untuk akun Anda.' + enable_success: '&2Mode premium diaktifkan! Anda tidak perlu lagi melakukan autentikasi saat login.' + not_enabled: '&eMode premium tidak diaktifkan untuk akun Anda.' + disable_success: '&2Mode premium dinonaktifkan. Anda perlu melakukan autentikasi kembali.' + error: '&cTerjadi kesalahan saat memverifikasi status premium Anda. Silakan coba lagi nanti.' + admin: + not_registered: '&c%name tidak terdaftar.' + already_enabled: '&eMode premium sudah diaktifkan untuk %name.' + account_not_found: '&cTidak ditemukan akun Mojang untuk %name.' + enable_success: '&2Mode premium diaktifkan untuk %name.' + not_enabled: '&eMode premium tidak diaktifkan untuk %name.' + disable_success: '&2Mode premium dinonaktifkan untuk %name.' + impostor_kicked: '&eSeorang pemain yang terhubung sebagai %name memiliki UUID yang tidak cocok dan telah dikeluarkan.' + kick_reason: '&cPengaturan premium akun Anda telah diubah oleh admin. Silakan sambungkan kembali.' + pending: '&eVerifikasi premium sedang menunggu untuk %name. Mereka harus terhubung kembali untuk mengonfirmasi kepemilikan akun Mojang.' + pending_kick: '&eVerifikasi premium diminta. Silakan terhubung kembali untuk mengonfirmasi kepemilikan akun Mojang Anda.' + pending_fail: '&cVerifikasi premium gagal. Silakan login dengan kata sandi Anda.' diff --git a/authme-core/src/main/resources/messages/messages_it.yml b/authme-core/src/main/resources/messages/messages_it.yml index 0e4c73bcb..680c4f28d 100644 --- a/authme-core/src/main/resources/messages/messages_it.yml +++ b/authme-core/src/main/resources/messages/messages_it.yml @@ -215,3 +215,25 @@ admin: first_set_fail: '&cSetFirstSpawn ha fallito, riprova.' not_defined: '&cSpawn fallito, prova a definire lo spawn.' first_not_defined: '&cPrimo spawn fallito, prova a definire il primo spawn.' + +# Modalità premium +premium: + feature_disabled: '&cLa modalità premium non è attivata su questo server.' + account_not_found: '&cNessun account Minecraft premium trovato per il tuo nome utente.' + already_enabled: '&eLa modalità premium è già attivata per il tuo account.' + enable_success: '&2Modalità premium attivata! Non dovrai più autenticarti al login.' + not_enabled: '&eLa modalità premium non è attivata per il tuo account.' + disable_success: '&2Modalità premium disattivata. Dovrai nuovamente autenticarti.' + error: '&cSi è verificato un errore durante la verifica del tuo stato premium. Riprova più tardi.' + admin: + not_registered: '&c%name non è registrato.' + already_enabled: '&eLa modalità premium è già attivata per %name.' + account_not_found: '&cNessun account Mojang trovato per %name.' + enable_success: '&2Modalità premium attivata per %name.' + not_enabled: '&eLa modalità premium non è attivata per %name.' + disable_success: '&2Modalità premium disattivata per %name.' + impostor_kicked: '&eUn giocatore connesso come %name aveva un UUID diverso ed è stato espulso.' + kick_reason: '&cLe impostazioni premium del tuo account sono state modificate da un amministratore. Riconnettiti.' + pending: '&eVerifica premium in sospeso per %name. Deve riconnettersi per confermare la proprietà dell''account Mojang.' + pending_kick: '&eVerifica premium richiesta. Riconnettiti per confermare la proprietà del tuo account Mojang.' + pending_fail: '&cVerifica premium non riuscita. Accedi con la tua password.' diff --git a/authme-core/src/main/resources/messages/messages_ja.yml b/authme-core/src/main/resources/messages/messages_ja.yml index 5ad77a6f9..5bd048792 100644 --- a/authme-core/src/main/resources/messages/messages_ja.yml +++ b/authme-core/src/main/resources/messages/messages_ja.yml @@ -213,3 +213,25 @@ admin: first_set_fail: '&cSetFirstSpawn が失敗しました。再試行してください。' not_defined: '&cスポーンに失敗しました。スポーンを定義してください。' first_not_defined: '&c最初のスポーンに失敗しました。最初のスポーンを定義してください。' + +# プレミアムモード +premium: + feature_disabled: '&cプレミアムモードはこのサーバーで有効になっていません。' + account_not_found: '&cあなたのユーザー名に対応するMinecraftプレミアムアカウントが見つかりませんでした。' + already_enabled: '&eプレミアムモードはすでにあなたのアカウントで有効です。' + enable_success: '&2プレミアムモードが有効になりました!ログイン時に認証する必要はなくなります。' + not_enabled: '&eプレミアムモードはあなたのアカウントで有効になっていません。' + disable_success: '&2プレミアムモードが無効になりました。再度認証が必要になります。' + error: '&cプレミアムステータスの確認中にエラーが発生しました。後でもう一度お試しください。' + admin: + not_registered: '&c%name は登録されていません。' + already_enabled: '&eプレミアムモードはすでに %name で有効です。' + account_not_found: '&c%name のMojangアカウントが見つかりませんでした。' + enable_success: '&2%name のプレミアムモードが有効になりました。' + not_enabled: '&eプレミアムモードは %name で有効になっていません。' + disable_success: '&2%name のプレミアムモードが無効になりました。' + impostor_kicked: '&e%name として接続していたプレイヤーのUUIDが一致せず、キックされました。' + kick_reason: '&cあなたのアカウントのプレミアム設定が管理者によって変更されました。再接続してください。' + pending: '&e%name のプレミアム確認が保留中です。Mojang アカウントの所有権を確認するために再接続する必要があります。' + pending_kick: '&eプレミアム確認が要求されました。Mojang アカウントの所有権を確認するために再接続してください。' + pending_fail: '&cプレミアム確認に失敗しました。パスワードでログインしてください。' diff --git a/authme-core/src/main/resources/messages/messages_ko.yml b/authme-core/src/main/resources/messages/messages_ko.yml index 8e0fb0cc6..6a25ff7c7 100644 --- a/authme-core/src/main/resources/messages/messages_ko.yml +++ b/authme-core/src/main/resources/messages/messages_ko.yml @@ -216,3 +216,25 @@ admin: first_set_fail: '&cSetFirstSpawn에 실패했습니다. 다시 시도해 주세요.' not_defined: '&c스폰에 실패했습니다. 스폰 포인트를 정의해 보세요.' first_not_defined: '&c첫 번째 스폰에 실패했습니다. 첫 번째 스폰 포인트를 정의해 보세요.' + +# 프리미엄 모드 +premium: + feature_disabled: '&c프리미엄 모드가 이 서버에서 활성화되어 있지 않습니다.' + account_not_found: '&c귀하의 사용자 이름에 대한 프리미엄 마인크래프트 계정을 찾을 수 없습니다.' + already_enabled: '&e프리미엄 모드가 이미 귀하의 계정에 활성화되어 있습니다.' + enable_success: '&2프리미엄 모드가 활성화되었습니다! 로그인 시 더 이상 인증할 필요가 없습니다.' + not_enabled: '&e프리미엄 모드가 귀하의 계정에 활성화되어 있지 않습니다.' + disable_success: '&2프리미엄 모드가 비활성화되었습니다. 다시 인증해야 합니다.' + error: '&c프리미엄 상태 확인 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.' + admin: + not_registered: '&c%name 은(는) 등록되어 있지 않습니다.' + already_enabled: '&e프리미엄 모드가 이미 %name 에 활성화되어 있습니다.' + account_not_found: '&c%name 에 대한 Mojang 계정을 찾을 수 없습니다.' + enable_success: '&2%name 의 프리미엄 모드가 활성화되었습니다.' + not_enabled: '&e프리미엄 모드가 %name 에 활성화되어 있지 않습니다.' + disable_success: '&2%name 의 프리미엄 모드가 비활성화되었습니다.' + impostor_kicked: '&e%name 으로 접속한 플레이어의 UUID가 일치하지 않아 추방되었습니다.' + kick_reason: '&c귀하 계정의 프리미엄 설정이 관리자에 의해 변경되었습니다. 다시 접속해 주세요.' + pending: '&e%name 에 대한 프리미엄 인증이 대기 중입니다. Mojang 계정 소유권을 확인하려면 다시 접속해야 합니다.' + pending_kick: '&e프리미엄 인증이 요청되었습니다. Mojang 계정 소유권을 확인하려면 다시 접속해 주세요.' + pending_fail: '&c프리미엄 인증에 실패했습니다. 비밀번호로 로그인해 주세요.' diff --git a/authme-core/src/main/resources/messages/messages_lt.yml b/authme-core/src/main/resources/messages/messages_lt.yml index e1c0dda24..21e04e481 100644 --- a/authme-core/src/main/resources/messages/messages_lt.yml +++ b/authme-core/src/main/resources/messages/messages_lt.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Premijų režimas +premium: + feature_disabled: '&cPremijų režimas neįjungtas šiame serveryje.' + account_not_found: '&cJūsų vartotojo vardui nerastas joks Minecraft premijų paskyros.' + already_enabled: '&ePremijų režimas jau įjungtas jūsų paskyroje.' + enable_success: '&2Premijų režimas įjungtas! Jums nebereikės autentifikuotis prisijungiant.' + not_enabled: '&ePremijų režimas neįjungtas jūsų paskyroje.' + disable_success: '&2Premijų režimas išjungtas. Turėsite vėl autentifikuotis.' + error: '&cTikrinant jūsų premijų būseną įvyko klaida. Bandykite vėliau.' + admin: + not_registered: '&c%name nėra užregistruotas.' + already_enabled: '&ePremijų režimas jau įjungtas %name.' + account_not_found: '&cMojang paskyra %name nerasta.' + enable_success: '&2Premijų režimas įjungtas %name.' + not_enabled: '&ePremijų režimas neįjungtas %name.' + disable_success: '&2Premijų režimas išjungtas %name.' + impostor_kicked: '&eŽaidėjas, prisijungęs kaip %name, turėjo nesutampantį UUID ir buvo išmestas.' + kick_reason: '&cJūsų paskyros premijų nustatymai buvo pakeisti administratoriaus. Prisijunkite iš naujo.' + pending: '&ePremijų patvirtinimas laukiamas %name. Jie turi prisijungti iš naujo, kad patvirtintų Mojang paskyros nuosavybę.' + pending_kick: '&ePremijų patvirtinimas paprašytas. Prisijunkite iš naujo, kad patvirtintumėte savo Mojang paskyros nuosavybę.' + pending_fail: '&cPremijų patvirtinimas nepavyko. Prisijunkite naudodami savo slaptažodį.' diff --git a/authme-core/src/main/resources/messages/messages_nl.yml b/authme-core/src/main/resources/messages/messages_nl.yml index 8047eca9e..5bd39144b 100644 --- a/authme-core/src/main/resources/messages/messages_nl.yml +++ b/authme-core/src/main/resources/messages/messages_nl.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn is mislukt, probeer het opnieuw.' not_defined: '&cSpawn mislukt, probeer het spawnpunt te definiëren.' first_not_defined: '&cEerste spawn mislukt, probeer het eerste spawnpunt te definiëren.' + +# Premium modus +premium: + feature_disabled: '&cDe premium modus is niet ingeschakeld op deze server.' + account_not_found: '&cGeen premium Minecraft-account gevonden voor jouw gebruikersnaam.' + already_enabled: '&eDe premium modus is al ingeschakeld voor jouw account.' + enable_success: '&2Premium modus ingeschakeld! Je hoeft je niet meer te authenticeren bij het inloggen.' + not_enabled: '&eDe premium modus is niet ingeschakeld voor jouw account.' + disable_success: '&2Premium modus uitgeschakeld. Je moet je opnieuw authenticeren.' + error: '&cEr is een fout opgetreden bij het controleren van jouw premium status. Probeer het later opnieuw.' + admin: + not_registered: '&c%name is niet geregistreerd.' + already_enabled: '&eDe premium modus is al ingeschakeld voor %name.' + account_not_found: '&cGeen Mojang-account gevonden voor %name.' + enable_success: '&2Premium modus ingeschakeld voor %name.' + not_enabled: '&eDe premium modus is niet ingeschakeld voor %name.' + disable_success: '&2Premium modus uitgeschakeld voor %name.' + impostor_kicked: '&eEen speler die verbonden was als %name had een afwijkende UUID en is gekickt.' + kick_reason: '&cDe premium instellingen van jouw account zijn gewijzigd door een beheerder. Verbind opnieuw.' + pending: '&ePremium verificatie in behandeling voor %name. Ze moeten opnieuw verbinden om het eigenaarschap van het Mojang-account te bevestigen.' + pending_kick: '&ePremium verificatie aangevraagd. Verbind opnieuw om het eigenaarschap van jouw Mojang-account te bevestigen.' + pending_fail: '&cPremium verificatie mislukt. Log in met jouw wachtwoord.' diff --git a/authme-core/src/main/resources/messages/messages_pl.yml b/authme-core/src/main/resources/messages/messages_pl.yml index 027541d87..08627d97f 100644 --- a/authme-core/src/main/resources/messages/messages_pl.yml +++ b/authme-core/src/main/resources/messages/messages_pl.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn nie powiódł się, spróbuj ponownie.' not_defined: '&cSpawn nie powiódł się, spróbuj zdefiniować spawn.' first_not_defined: '&cPierwszy spawn nie powiódł się, spróbuj zdefiniować pierwszy spawn.' + +# Tryb premium +premium: + feature_disabled: '&cTryb premium nie jest włączony na tym serwerze.' + account_not_found: '&cNie znaleziono konta Minecraft premium dla Twojej nazwy użytkownika.' + already_enabled: '&eTryb premium jest już włączony dla Twojego konta.' + enable_success: '&2Tryb premium włączony! Nie będziesz już musiał się uwierzytelniać przy logowaniu.' + not_enabled: '&eTryb premium nie jest włączony dla Twojego konta.' + disable_success: '&2Tryb premium wyłączony. Będziesz musiał się ponownie uwierzytelniać.' + error: '&cWystąpił błąd podczas weryfikacji Twojego statusu premium. Spróbuj ponownie później.' + admin: + not_registered: '&c%name nie jest zarejestrowany.' + already_enabled: '&eTryb premium jest już włączony dla %name.' + account_not_found: '&cNie znaleziono konta Mojang dla %name.' + enable_success: '&2Tryb premium włączony dla %name.' + not_enabled: '&eTryb premium nie jest włączony dla %name.' + disable_success: '&2Tryb premium wyłączony dla %name.' + impostor_kicked: '&eGracz połączony jako %name miał inny UUID i został wyrzucony.' + kick_reason: '&cUstawienia premium Twojego konta zostały zmienione przez administratora. Połącz się ponownie.' + pending: '&eWeryfikacja premium oczekuje dla %name. Muszą się ponownie połączyć, aby potwierdzić właściciela konta Mojang.' + pending_kick: '&eZażądano weryfikacji premium. Połącz się ponownie, aby potwierdzić właściciela swojego konta Mojang.' + pending_fail: '&cWeryfikacja premium nie powiodła się. Zaloguj się za pomocą hasła.' diff --git a/authme-core/src/main/resources/messages/messages_pt.yml b/authme-core/src/main/resources/messages/messages_pt.yml index ec740eb11..434961934 100644 --- a/authme-core/src/main/resources/messages/messages_pt.yml +++ b/authme-core/src/main/resources/messages/messages_pt.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn falhou, por favor tente novamente.' not_defined: '&cSpawn falhou, por favor tente definir o spawn.' first_not_defined: '&cPrimeiro spawn falhou, por favor tente definir o primeiro spawn.' + +# Modo premium +premium: + feature_disabled: '&cO modo premium não está ativado neste servidor.' + account_not_found: '&cNenhuma conta Minecraft premium encontrada para o seu nome de utilizador.' + already_enabled: '&eO modo premium já está ativado para a sua conta.' + enable_success: '&2Modo premium ativado! Já não necessitará de se autenticar ao entrar.' + not_enabled: '&eO modo premium não está ativado para a sua conta.' + disable_success: '&2Modo premium desativado. Terá de se autenticar novamente.' + error: '&cOcorreu um erro ao verificar o seu estado premium. Por favor, tente novamente mais tarde.' + admin: + not_registered: '&c%name não está registado.' + already_enabled: '&eO modo premium já está ativado para %name.' + account_not_found: '&cNenhuma conta Mojang encontrada para %name.' + enable_success: '&2Modo premium ativado para %name.' + not_enabled: '&eO modo premium não está ativado para %name.' + disable_success: '&2Modo premium desativado para %name.' + impostor_kicked: '&eUm jogador ligado como %name tinha um UUID diferente e foi expulso.' + kick_reason: '&cAs definições premium da sua conta foram alteradas por um administrador. Por favor, ligue-se novamente.' + pending: '&eVerificação premium pendente para %name. Têm de se reconectar para confirmar a propriedade da conta Mojang.' + pending_kick: '&eVerificação premium solicitada. Por favor, reconecte-se para confirmar a propriedade da sua conta Mojang.' + pending_fail: '&cVerificação premium falhou. Por favor, inicie sessão com a sua palavra-passe.' diff --git a/authme-core/src/main/resources/messages/messages_ro.yml b/authme-core/src/main/resources/messages/messages_ro.yml index 485d11450..3725912f5 100644 --- a/authme-core/src/main/resources/messages/messages_ro.yml +++ b/authme-core/src/main/resources/messages/messages_ro.yml @@ -213,3 +213,25 @@ admin: first_set_fail: '&cSetFirstSpawn a eșuat, te rugăm să reîncerci.' not_defined: '&cSpawn-ul a eșuat, te rugăm să încearcă să definești spawn-ul.' first_not_defined: '&cPrimul spawn a eșuat, te rugăm să încearcă să definești primul spawn.' + +# Modul premium +premium: + feature_disabled: '&cModul premium nu este activat pe acest server.' + account_not_found: '&cNu a fost găsit niciun cont Minecraft premium pentru numele tău de utilizator.' + already_enabled: '&eModul premium este deja activat pentru contul tău.' + enable_success: '&2Modul premium activat! Nu va mai trebui să te autentifici la conectare.' + not_enabled: '&eModul premium nu este activat pentru contul tău.' + disable_success: '&2Modul premium dezactivat. Va trebui să te autentifici din nou.' + error: '&cA apărut o eroare la verificarea statusului tău premium. Te rugăm să încerci din nou mai târziu.' + admin: + not_registered: '&c%name nu este înregistrat.' + already_enabled: '&eModul premium este deja activat pentru %name.' + account_not_found: '&cNu a fost găsit niciun cont Mojang pentru %name.' + enable_success: '&2Modul premium activat pentru %name.' + not_enabled: '&eModul premium nu este activat pentru %name.' + disable_success: '&2Modul premium dezactivat pentru %name.' + impostor_kicked: '&eUn jucător conectat ca %name a avut un UUID diferit și a fost dat afară.' + kick_reason: '&cSetările premium ale contului tău au fost modificate de un administrator. Te rugăm să te reconectezi.' + pending: '&eVerificarea premium este în așteptare pentru %name. Trebuie să se reconecteze pentru a confirma proprietatea contului Mojang.' + pending_kick: '&eVerificare premium solicitată. Te rugăm să te reconectezi pentru a confirma proprietatea contului tău Mojang.' + pending_fail: '&cVerificarea premium a eșuat. Te rugăm să te conectezi cu parola ta.' diff --git a/authme-core/src/main/resources/messages/messages_ru.yml b/authme-core/src/main/resources/messages/messages_ru.yml index 859ff676b..b5c6ea63b 100644 --- a/authme-core/src/main/resources/messages/messages_ru.yml +++ b/authme-core/src/main/resources/messages/messages_ru.yml @@ -209,3 +209,25 @@ admin: first_set_fail: '&cSetFirstSpawn не удался, попробуйте ещё раз.' not_defined: '&cСпавн не удался, попробуйте определить точку спавна.' first_not_defined: '&cПервый спавн не удался, попробуйте определить первую точку спавна.' + +# Режим Premium +premium: + feature_disabled: '&cРежим Premium не включён на этом сервере.' + account_not_found: '&cПремиум-аккаунт Minecraft для вашего имени пользователя не найден.' + already_enabled: '&eРежим Premium уже включён для вашего аккаунта.' + enable_success: '&2Режим Premium включён! Вам больше не нужно проходить аутентификацию при входе.' + not_enabled: '&eРежим Premium не включён для вашего аккаунта.' + disable_success: '&2Режим Premium отключён. Вам снова потребуется аутентификация.' + error: '&cПри проверке вашего Premium-статуса произошла ошибка. Повторите попытку позже.' + admin: + not_registered: '&c%name не зарегистрирован.' + already_enabled: '&eРежим Premium уже включён для %name.' + account_not_found: '&cАккаунт Mojang для %name не найден.' + enable_success: '&2Режим Premium включён для %name.' + not_enabled: '&eРежим Premium не включён для %name.' + disable_success: '&2Режим Premium отключён для %name.' + impostor_kicked: '&eИгрок, подключённый как %name, имел несовпадающий UUID и был исключён с сервера.' + kick_reason: '&cНастройки Premium вашего аккаунта были изменены администратором. Пожалуйста, переподключитесь.' + pending: '&ePremium-верификация ожидает подтверждения для %name. Необходимо переподключиться для подтверждения владения аккаунтом Mojang.' + pending_kick: '&eЗапрошена Premium-верификация. Пожалуйста, переподключитесь для подтверждения владения вашим аккаунтом Mojang.' + pending_fail: '&cPremium-верификация не удалась. Пожалуйста, войдите с помощью пароля.' diff --git a/authme-core/src/main/resources/messages/messages_si.yml b/authme-core/src/main/resources/messages/messages_si.yml index 731b39183..540235459 100644 --- a/authme-core/src/main/resources/messages/messages_si.yml +++ b/authme-core/src/main/resources/messages/messages_si.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Premium način +premium: + feature_disabled: '&cPremium način ni omogočen na tem strežniku.' + account_not_found: '&cZa vaše uporabniško ime ni bilo najdenega nobenega Minecraft premium računa.' + already_enabled: '&ePremium način je že omogočen za vaš račun.' + enable_success: '&2Premium način omogočen! Pri prijavi se vam ne bo več treba preverjati.' + not_enabled: '&ePremium način ni omogočen za vaš račun.' + disable_success: '&2Premium način onemogočen. Znova se boste morali preverjati.' + error: '&cPri preverjanju vašega premium statusa je prišlo do napake. Poskusite znova pozneje.' + admin: + not_registered: '&c%name ni registriran.' + already_enabled: '&ePremium način je že omogočen za %name.' + account_not_found: '&cZa %name ni bil najden noben račun Mojang.' + enable_success: '&2Premium način omogočen za %name.' + not_enabled: '&ePremium način ni omogočen za %name.' + disable_success: '&2Premium način onemogočen za %name.' + impostor_kicked: '&eIgralec, ki je bil povezan kot %name, je imel neujemajoč UUID in je bil izključen.' + kick_reason: '&cPremium nastavitve vašega računa je spremenil skrbnik. Prosimo, povežite se znova.' + pending: '&ePremium preverjanje je v teku za %name. Znova se morajo povezati, da potrdijo lastništvo računa Mojang.' + pending_kick: '&eZahtevano je premium preverjanje. Prosimo, povežite se znova, da potrdite lastništvo svojega računa Mojang.' + pending_fail: '&cPremium preverjanje ni uspelo. Prosimo, prijavite se z geslom.' diff --git a/authme-core/src/main/resources/messages/messages_sk.yml b/authme-core/src/main/resources/messages/messages_sk.yml index 767b14dc2..f44aa9891 100644 --- a/authme-core/src/main/resources/messages/messages_sk.yml +++ b/authme-core/src/main/resources/messages/messages_sk.yml @@ -220,3 +220,25 @@ admin: first_set_fail: '&cSetFirstSpawn zlyhal, prosím skús to znova.' not_defined: '&cSpawn zlyhal, prosím skús definovať spawn.' first_not_defined: '&cPrvý spawn zlyhal, prosím skús definovať prvý spawn.' + +# Prémiový režim +premium: + feature_disabled: '&cPrémiový režim nie je povolený na tomto serveri.' + account_not_found: '&cPre toto používateľské meno nebol nájdený žiadny prémiový účet Minecraft.' + already_enabled: '&ePrémiový režim je pre váš účet už povolený.' + enable_success: '&2Prémiový režim bol aktivovaný! Pri prihlasovaní sa už nebudete musieť overovať.' + not_enabled: '&ePrémiový režim nie je pre váš účet povolený.' + disable_success: '&2Prémiový režim bol deaktivovaný. Budete sa musieť znova overiť.' + error: '&cPri overovaní vášho prémiového stavu došlo k chybe. Skúste to prosím neskôr.' + admin: + not_registered: '&c%name nie je zaregistrovaný.' + already_enabled: '&ePrémiový režim je pre %name už povolený.' + account_not_found: '&cPre %name nebol nájdený žiadny účet Mojang.' + enable_success: '&2Prémiový režim bol aktivovaný pre %name.' + not_enabled: '&ePrémiový režim nie je pre %name povolený.' + disable_success: '&2Prémiový režim bol deaktivovaný pre %name.' + impostor_kicked: '&eHráč pripojený ako %name mal nezhodujúce sa UUID a bol vyhodený.' + kick_reason: '&cNastavenia premium vášho účtu boli zmenené administrátorom. Prosím, znova sa pripojte.' + pending: '&eOverenie premium čaká pre %name. Musia sa znova pripojiť, aby potvrdili vlastníctvo účtu Mojang.' + pending_kick: '&eBolo požiadané overenie premium. Prosím, znova sa pripojte, aby ste potvrdili vlastníctvo svojho účtu Mojang.' + pending_fail: '&cOverenie premium zlyhalo. Prosím, prihláste sa pomocou hesla.' diff --git a/authme-core/src/main/resources/messages/messages_sr.yml b/authme-core/src/main/resources/messages/messages_sr.yml index c16261d00..436b21256 100644 --- a/authme-core/src/main/resources/messages/messages_sr.yml +++ b/authme-core/src/main/resources/messages/messages_sr.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn has failed, please retry.' not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' + +# Премиум режим +premium: + feature_disabled: '&cПремиум режим није омогућен на овом серверу.' + account_not_found: '&cНије пронађен Minecraft премиум налог за ваше корисничко име.' + already_enabled: '&eПремиум режим је већ омогућен за ваш налог.' + enable_success: '&2Премиум режим омогућен! Више нећете морати да се аутентификујете при пријави.' + not_enabled: '&eПремиум режим није омогућен за ваш налог.' + disable_success: '&2Премиум режим онемогућен. Морате поново да се аутентификујете.' + error: '&cДошло је до грешке при провери вашег премиум статуса. Покушајте поново касније.' + admin: + not_registered: '&c%name није регистрован.' + already_enabled: '&eПремиум режим је већ омогућен за %name.' + account_not_found: '&cMojang налог за %name није пронађен.' + enable_success: '&2Премиум режим омогућен за %name.' + not_enabled: '&eПремиум режим није омогућен за %name.' + disable_success: '&2Премиум режим онемогућен за %name.' + impostor_kicked: '&eИграч повезан као %name имао је неподударајући UUID и избачен је.' + kick_reason: '&cПремиум подешавања вашег налога је променио администратор. Поново се повежите.' + pending: '&eПремиум верификација чека за %name. Морају се поново повезати да потврде власништво над Mojang налогом.' + pending_kick: '&eЗатражена је премиум верификација. Поново се повежите да потврдите власништво над вашим Mojang налогом.' + pending_fail: '&cПремиум верификација није успела. Молимо пријавите се лозинком.' diff --git a/authme-core/src/main/resources/messages/messages_tr.yml b/authme-core/src/main/resources/messages/messages_tr.yml index 548a23e3b..36913490d 100644 --- a/authme-core/src/main/resources/messages/messages_tr.yml +++ b/authme-core/src/main/resources/messages/messages_tr.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn başarısız oldu, lütfen tekrar deneyin.' not_defined: '&cDoğma başarısız oldu, lütfen doğma noktasını tanımlamayı deneyin.' first_not_defined: '&cİlk doğma başarısız oldu, lütfen ilk doğma noktasını tanımlamayı deneyin.' + +# Premium modu +premium: + feature_disabled: '&cPremium modu bu sunucuda etkin değil.' + account_not_found: '&cKullanıcı adınız için premium Minecraft hesabı bulunamadı.' + already_enabled: '&ePremium modu hesabınız için zaten etkin.' + enable_success: '&2Premium modu etkinleştirildi! Artık giriş yaparken kimlik doğrulamanız gerekmeyecek.' + not_enabled: '&ePremium modu hesabınız için etkin değil.' + disable_success: '&2Premium modu devre dışı bırakıldı. Tekrar kimlik doğrulamanız gerekecek.' + error: '&cPremium durumunuz doğrulanırken bir hata oluştu. Lütfen daha sonra tekrar deneyin.' + admin: + not_registered: '&c%name kayıtlı değil.' + already_enabled: '&ePremium modu %name için zaten etkin.' + account_not_found: '&c%name için Mojang hesabı bulunamadı.' + enable_success: '&2%name için premium modu etkinleştirildi.' + not_enabled: '&ePremium modu %name için etkin değil.' + disable_success: '&2%name için premium modu devre dışı bırakıldı.' + impostor_kicked: '&e%name olarak bağlanan oyuncunun UUID''si uyuşmuyordu ve sunucudan atıldı.' + kick_reason: '&cHesabınızın premium ayarları bir yönetici tarafından değiştirildi. Lütfen yeniden bağlanın.' + pending: '&e%name için premium doğrulama bekleniyor. Mojang hesabının sahipliğini onaylamak için yeniden bağlanmaları gerekiyor.' + pending_kick: '&ePremium doğrulama istendi. Mojang hesabınızın sahipliğini onaylamak için lütfen yeniden bağlanın.' + pending_fail: '&cPremium doğrulama başarısız oldu. Lütfen şifrenizle giriş yapın.' diff --git a/authme-core/src/main/resources/messages/messages_uk.yml b/authme-core/src/main/resources/messages/messages_uk.yml index dfc71c39e..9f1418ce8 100644 --- a/authme-core/src/main/resources/messages/messages_uk.yml +++ b/authme-core/src/main/resources/messages/messages_uk.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn не вдався, спробуйте ще раз.' not_defined: '&cСпавн не вдався, спробуйте визначити точку спавну.' first_not_defined: '&cПерший спавн не вдався, спробуйте визначити першу точку спавну.' + +# Режим Premium +premium: + feature_disabled: '&cРежим Premium не ввімкнено на цьому сервері.' + account_not_found: '&cПреміум-акаунт Minecraft для вашого імені користувача не знайдено.' + already_enabled: '&eРежим Premium вже ввімкнено для вашого акаунта.' + enable_success: '&2Режим Premium увімкнено! Вам більше не потрібно проходити автентифікацію при вході.' + not_enabled: '&eРежим Premium не ввімкнено для вашого акаунта.' + disable_success: '&2Режим Premium вимкнено. Вам знову потрібно буде пройти автентифікацію.' + error: '&cПід час перевірки вашого Premium-статусу сталася помилка. Будь ласка, спробуйте пізніше.' + admin: + not_registered: '&c%name не зареєстрований.' + already_enabled: '&eРежим Premium вже ввімкнено для %name.' + account_not_found: '&cАкаунт Mojang для %name не знайдено.' + enable_success: '&2Режим Premium увімкнено для %name.' + not_enabled: '&eРежим Premium не ввімкнено для %name.' + disable_success: '&2Режим Premium вимкнено для %name.' + impostor_kicked: '&eГравець, підключений як %name, мав невідповідний UUID і був виключений з сервера.' + kick_reason: '&cНалаштування Premium вашого акаунта були змінені адміністратором. Будь ласка, підключіться знову.' + pending: '&eПеревірка Premium очікується для %name. Вони повинні повторно підключитися, щоб підтвердити право власності на акаунт Mojang.' + pending_kick: '&eЗапитано перевірку Premium. Будь ласка, повторно підключіться, щоб підтвердити право власності на ваш акаунт Mojang.' + pending_fail: '&cПеревірка Premium не вдалася. Будь ласка, увійдіть за допомогою пароля.' diff --git a/authme-core/src/main/resources/messages/messages_vn.yml b/authme-core/src/main/resources/messages/messages_vn.yml index 31a0eaccf..1f4efe0b6 100644 --- a/authme-core/src/main/resources/messages/messages_vn.yml +++ b/authme-core/src/main/resources/messages/messages_vn.yml @@ -215,3 +215,25 @@ admin: not_defined: '&cSpawn has failed, please try to define the spawn.' first_not_defined: '&cFirst spawn has failed, please try to define the first spawn.' +# Chế độ premium +premium: + feature_disabled: '&cChế độ premium không được bật trên máy chủ này.' + account_not_found: '&cKhông tìm thấy tài khoản Minecraft premium nào cho tên người dùng của bạn.' + already_enabled: '&eChế độ premium đã được bật cho tài khoản của bạn.' + enable_success: '&2Chế độ premium đã được bật! Bạn sẽ không cần xác thực khi đăng nhập nữa.' + not_enabled: '&eChế độ premium chưa được bật cho tài khoản của bạn.' + disable_success: '&2Chế độ premium đã bị tắt. Bạn sẽ cần xác thực lại.' + error: '&cĐã xảy ra lỗi khi xác minh trạng thái premium của bạn. Vui lòng thử lại sau.' + admin: + not_registered: '&c%name chưa đăng ký.' + already_enabled: '&eChế độ premium đã được bật cho %name.' + account_not_found: '&cKhông tìm thấy tài khoản Mojang cho %name.' + enable_success: '&2Chế độ premium đã được bật cho %name.' + not_enabled: '&eChế độ premium chưa được bật cho %name.' + disable_success: '&2Chế độ premium đã bị tắt cho %name.' + impostor_kicked: '&eMột người chơi kết nối với tên %name có UUID không khớp và đã bị đuổi.' + kick_reason: '&cCài đặt premium tài khoản của bạn đã được thay đổi bởi quản trị viên. Vui lòng kết nối lại.' + pending: '&eĐang chờ xác minh premium cho %name. Họ phải kết nối lại để xác nhận quyền sở hữu tài khoản Mojang.' + pending_kick: '&eĐã yêu cầu xác minh premium. Vui lòng kết nối lại để xác nhận quyền sở hữu tài khoản Mojang của bạn.' + pending_fail: '&cXác minh premium thất bại. Vui lòng đăng nhập bằng mật khẩu của bạn.' + diff --git a/authme-core/src/main/resources/messages/messages_zhcn.yml b/authme-core/src/main/resources/messages/messages_zhcn.yml index 2bef18bbf..56ead4270 100644 --- a/authme-core/src/main/resources/messages/messages_zhcn.yml +++ b/authme-core/src/main/resources/messages/messages_zhcn.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn 失败,请重试。' not_defined: '&c出生失败,请尝试定义出生点。' first_not_defined: '&c第一出生失败,请尝试定义第一出生点。' + +# 正版模式 +premium: + feature_disabled: '&c此服务器未启用正版模式。' + account_not_found: '&c未找到与您用户名对应的正版 Minecraft 账户。' + already_enabled: '&e您的账户已启用正版模式。' + enable_success: '&2正版模式已启用!登录时将不再需要进行身份验证。' + not_enabled: '&e您的账户未启用正版模式。' + disable_success: '&2正版模式已禁用。您将需要重新进行身份验证。' + error: '&c验证您的正版状态时发生错误。请稍后重试。' + admin: + not_registered: '&c%name 未注册。' + already_enabled: '&e%name 的正版模式已启用。' + account_not_found: '&c未找到 %name 的 Mojang 账户。' + enable_success: '&2%name 的正版模式已启用。' + not_enabled: '&e%name 的正版模式未启用。' + disable_success: '&2%name 的正版模式已禁用。' + impostor_kicked: '&e以 %name 身份连接的玩家 UUID 不匹配,已被踢出。' + kick_reason: '&c您账户的正版设置已被管理员更改。请重新连接。' + pending: '&e正在等待 %name 的正版验证。他们必须重新连接以确认 Mojang 账户的所有权。' + pending_kick: '&e已请求正版验证。请重新连接以确认您的 Mojang 账户所有权。' + pending_fail: '&c正版验证失败。请使用密码登录。' diff --git a/authme-core/src/main/resources/messages/messages_zhhk.yml b/authme-core/src/main/resources/messages/messages_zhhk.yml index ca9b082ee..a9d609017 100644 --- a/authme-core/src/main/resources/messages/messages_zhhk.yml +++ b/authme-core/src/main/resources/messages/messages_zhhk.yml @@ -217,3 +217,25 @@ admin: first_set_fail: '&cSetFirstSpawn 失敗,請重試。' not_defined: '&c出生失敗,請嘗試定義出生點。' first_not_defined: '&c第一出生失敗,請嘗試定義第一出生點。' + +# 正版模式 +premium: + feature_disabled: '&c此伺服器未啟用正版模式。' + account_not_found: '&c找不到與您用戶名對應的正版 Minecraft 帳戶。' + already_enabled: '&e您的帳戶已啟用正版模式。' + enable_success: '&2正版模式已啟用!登入時將不再需要進行身份驗證。' + not_enabled: '&e您的帳戶未啟用正版模式。' + disable_success: '&2正版模式已停用。您將需要重新進行身份驗證。' + error: '&c驗證您的正版狀態時發生錯誤。請稍後再試。' + admin: + not_registered: '&c%name 未註冊。' + already_enabled: '&e%name 的正版模式已啟用。' + account_not_found: '&c找不到 %name 的 Mojang 帳戶。' + enable_success: '&2%name 的正版模式已啟用。' + not_enabled: '&e%name 的正版模式未啟用。' + disable_success: '&2%name 的正版模式已停用。' + impostor_kicked: '&e以 %name 身份連接的玩家 UUID 不符,已被踢出。' + kick_reason: '&c您帳戶的正版設定已被管理員更改。請重新連接。' + pending: '&e正在等待 %name 的正版驗證。他們必須重新連接以確認 Mojang 帳戶的所有權。' + pending_kick: '&e已請求正版驗證。請重新連接以確認您的 Mojang 帳戶所有權。' + pending_fail: '&c正版驗證失敗。請使用密碼登入。' diff --git a/authme-core/src/main/resources/messages/messages_zhmc.yml b/authme-core/src/main/resources/messages/messages_zhmc.yml index cacaf0a8d..86b639027 100644 --- a/authme-core/src/main/resources/messages/messages_zhmc.yml +++ b/authme-core/src/main/resources/messages/messages_zhmc.yml @@ -214,3 +214,25 @@ admin: first_set_fail: '&cSetFirstSpawn 失敗,請重試。' not_defined: '&c出生失敗,請嘗試定義出生點。' first_not_defined: '&c第一出生失敗,請嘗試定義第一出生點。' + +# 正版模式 +premium: + feature_disabled: '&c此伺服器未啟用正版模式。' + account_not_found: '&c找不到與您用戶名對應的正版 Minecraft 帳戶。' + already_enabled: '&e您的帳戶已啟用正版模式。' + enable_success: '&2正版模式已啟用!登入時將不再需要進行身份驗證。' + not_enabled: '&e您的帳戶未啟用正版模式。' + disable_success: '&2正版模式已停用。您將需要重新進行身份驗證。' + error: '&c驗證您的正版狀態時發生錯誤。請稍後再試。' + admin: + not_registered: '&c%name 未註冊。' + already_enabled: '&e%name 的正版模式已啟用。' + account_not_found: '&c找不到 %name 的 Mojang 帳戶。' + enable_success: '&2%name 的正版模式已啟用。' + not_enabled: '&e%name 的正版模式未啟用。' + disable_success: '&2%name 的正版模式已停用。' + impostor_kicked: '&e以 %name 身份連接的玩家 UUID 不符,已被踢出。' + kick_reason: '&c您帳戶的正版設定已被管理員更改。請重新連接。' + pending: '&e正在等待 %name 的正版驗證。他們必須重新連接以確認 Mojang 帳戶的所有權。' + pending_kick: '&e已請求正版驗證。請重新連接以確認您的 Mojang 帳戶所有權。' + pending_fail: '&c正版驗證失敗。請使用密碼登入。' diff --git a/authme-core/src/main/resources/messages/messages_zhtw.yml b/authme-core/src/main/resources/messages/messages_zhtw.yml index e21b63013..9a9cc49ab 100644 --- a/authme-core/src/main/resources/messages/messages_zhtw.yml +++ b/authme-core/src/main/resources/messages/messages_zhtw.yml @@ -216,3 +216,25 @@ admin: first_set_fail: '&cSetFirstSpawn 失敗,請重試。' not_defined: '&c出生失敗,請嘗試定義出生點。' first_not_defined: '&c第一出生失敗,請嘗試定義第一出生點。' + +# 正版模式 +premium: + feature_disabled: '&c此伺服器未啟用正版模式。' + account_not_found: '&c找不到與您使用者名稱對應的正版 Minecraft 帳號。' + already_enabled: '&e您的帳號已啟用正版模式。' + enable_success: '&2正版模式已啟用!登入時將不再需要進行身份驗證。' + not_enabled: '&e您的帳號未啟用正版模式。' + disable_success: '&2正版模式已停用。您將需要重新進行身份驗證。' + error: '&c驗證您的正版狀態時發生錯誤。請稍後再試。' + admin: + not_registered: '&c%name 未註冊。' + already_enabled: '&e%name 的正版模式已啟用。' + account_not_found: '&c找不到 %name 的 Mojang 帳號。' + enable_success: '&2%name 的正版模式已啟用。' + not_enabled: '&e%name 的正版模式未啟用。' + disable_success: '&2%name 的正版模式已停用。' + impostor_kicked: '&e以 %name 身份連線的玩家 UUID 不符,已被踢出。' + kick_reason: '&c您帳號的正版設定已被管理員更改。請重新連線。' + pending: '&e正在等待 %name 的正版驗證。他們必須重新連線以確認 Mojang 帳號的所有權。' + pending_kick: '&e已請求正版驗證。請重新連線以確認您的 Mojang 帳號所有權。' + pending_fail: '&c正版驗證失敗。請使用密碼登入。' diff --git a/authme-core/src/main/resources/plugin.yml b/authme-core/src/main/resources/plugin.yml index ea47e0d5b..dae4cf090 100644 --- a/authme-core/src/main/resources/plugin.yml +++ b/authme-core/src/main/resources/plugin.yml @@ -39,7 +39,7 @@ libraries: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|premium|freemium|debug email: description: Add email or recover password usage: /email show|add|change|recover|code|setpassword @@ -79,6 +79,12 @@ commands: verification: description: Verification command usage: /verification + premium: + description: Enable premium mode + usage: /premium + freemium: + description: Disable premium mode + usage: /freemium permissions: authme.admin.*: description: Gives access to all admin commands @@ -103,6 +109,8 @@ permissions: authme.admin.seeotheraccounts: true authme.admin.seerecent: true authme.admin.setfirstspawn: true + authme.admin.setfreemium: true + authme.admin.setpremium: true authme.admin.setspawn: true authme.admin.spawn: true authme.admin.switchantibot: true @@ -170,6 +178,12 @@ permissions: authme.admin.setfirstspawn: description: Administrator command to set the first AuthMe spawn. default: op + authme.admin.setfreemium: + description: Administrator command to disable premium mode for a player. + default: op + authme.admin.setpremium: + description: Administrator command to enable premium mode for a player. + default: op authme.admin.setspawn: description: Administrator command to set the AuthMe spawn. default: op @@ -270,8 +284,10 @@ permissions: authme.player.email.change: true authme.player.email.recover: true authme.player.email.see: true + authme.player.freemium: true authme.player.login: true authme.player.logout: true + authme.player.premium: true authme.player.protection.quickcommandsprotection: true authme.player.register: true authme.player.security.verificationcode: true @@ -307,12 +323,19 @@ permissions: authme.player.email.see: description: Command permission to see the own email address. default: true + authme.player.freemium: + description: Permission to disable premium mode. + default: true authme.player.login: description: Command permission to login. default: true authme.player.logout: description: Command permission to logout. default: true + authme.player.premium: + description: Permission to enable premium mode (skip authentication using a verified + Mojang account). + default: true authme.player.protection.quickcommandsprotection: description: Permission that enables on join quick commands checks for the player. default: true diff --git a/authme-core/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/authme-core/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index e2bb00cf9..87b7a3f34 100644 --- a/authme-core/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -45,7 +45,7 @@ void shouldInitializeCommands() { // It obviously doesn't make sense to test much of the concrete data // that is being initialized; we just want to guarantee with this test // that data is indeed being initialized and we take a few "probes" - assertThat(commands, hasSize(10)); + assertThat(commands, hasSize(12)); assertThat(commandsIncludeLabel(commands, "authme"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "register"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "help"), equalTo(false)); diff --git a/authme-core/src/test/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapterTest.java b/authme-core/src/test/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapterTest.java index 642e5e852..4753ac62c 100644 --- a/authme-core/src/test/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapterTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/platform/AbstractSpigotPlatformAdapterTest.java @@ -2,6 +2,8 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumLoginVerifier; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; @@ -119,5 +121,14 @@ public void registerTabCompleteBlock(PlayerCache playerCache) { public void unregisterTabCompleteBlock() { tabCompleteUnregistrations++; } + + @Override + public void registerPremiumVerification(DataSource dataSource, PremiumLoginVerifier verifier, + PendingPremiumCache pendingPremiumCache) { + } + + @Override + public void unregisterPremiumVerification() { + } } } diff --git a/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java b/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java index b33132193..ff6ea93eb 100644 --- a/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/process/join/AsynchronousJoinTest.java @@ -13,6 +13,7 @@ import fr.xephi.authme.service.PreJoinDialogService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PremiumLoginVerifier; import fr.xephi.authme.service.DialogStateService; import fr.xephi.authme.service.DialogWindowService; import fr.xephi.authme.service.PluginHookService; @@ -22,6 +23,7 @@ import fr.xephi.authme.settings.WelcomeMessageConfiguration; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.PremiumSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Player; @@ -92,6 +94,8 @@ public class AsynchronousJoinTest { private DialogStateService dialogStateService; @Mock private PreJoinDialogService preJoinDialogService; + @Mock + private PremiumLoginVerifier premiumLoginVerifier; @BeforeAll public static void initLogger() { @@ -103,6 +107,7 @@ public void setUp() { setBukkitServiceToScheduleSyncTaskFromOptionallyAsyncTask(bukkitService); setBukkitServiceToRunTaskOptionallyAsync(bukkitService); setBukkitServiceToRunTaskLater(bukkitService); + given(service.getProperty(PremiumSettings.ENABLE_PREMIUM)).willReturn(false); } @Test @@ -126,7 +131,7 @@ public void shouldShowLoginDialogForUnauthenticatedRegisteredPlayer() { @Test public void shouldNotShowLoginDialogForAlreadyAuthenticatedPlayer() { - // given + // given — player is already authenticated when the sync limbo task fires Player player = mockPlayer("Bobby"); setUpRegisteredJoin(player); given(playerCache.isAuthenticated("Bobby")).willReturn(true); @@ -134,18 +139,23 @@ public void shouldNotShowLoginDialogForAlreadyAuthenticatedPlayer() { // when asynchronousJoin.processJoin(player); - // then - verify(limboService).createLimboPlayer(player, true); + // then — sync guard detects authenticated state; limbo and dialog are both skipped + verify(limboService, never()).createLimboPlayer(eq(player), eq(true)); verify(dialogAdapter, never()).showLoginDialog(eq(player), any(DialogWindowSpec.class)); } @Test public void shouldNotShowDelayedDialogIfPlayerGetsAuthenticatedBeforeTaskRuns() { - // given + // given — player is NOT yet authenticated when the sync guard and dialog-condition run, + // but IS authenticated by the time the 1-tick delayed task executes. + // Stub sequence (all calls use "Bobby" = player.getName(), not lowercased): + // call 1 → sync guard → false (continues to createLimboPlayer) + // call 2 → !isAuthenticated() in cond → false (condition passes, schedules delayed task) + // call 3 → inside showPostJoinDialog → true (dialog suppressed) Player player = mockPlayer("Bobby"); setUpRegisteredJoin(player); given(player.isOnline()).willReturn(true); - given(playerCache.isAuthenticated("Bobby")).willReturn(false, true); + given(playerCache.isAuthenticated("Bobby")).willReturn(false, false, true); given(service.getProperty(RegistrationSettings.USE_DIALOG_UI)).willReturn(true); given(dialogAdapter.isDialogSupported()).willReturn(true); given(dialogWindowService.createLoginDialog(player)).willReturn(createDialogSpec("Login", "Login")); @@ -160,7 +170,8 @@ public void shouldNotShowDelayedDialogIfPlayerGetsAuthenticatedBeforeTaskRuns() asynchronousJoin.processJoin(player); delayedTask.get().run(); - // then + // then — limbo was created (player was not authenticated when guard ran), + // but dialog was suppressed because player became authenticated before delayed task ran verify(limboService).createLimboPlayer(player, true); verify(dialogAdapter, never()).showLoginDialog(eq(player), any(DialogWindowSpec.class)); } 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 037180574..33d13e4a7 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 @@ -4,9 +4,12 @@ import com.google.common.io.ByteStreams; import fr.xephi.authme.AuthMe; import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.HashUtils; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PendingPremiumCache; +import fr.xephi.authme.service.PremiumService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import org.bukkit.Server; @@ -46,6 +49,15 @@ class BungeeReceiverTest { @Mock private BungeeSender bungeeSender; + @Mock + private DataSource dataSource; + + @Mock + private PendingPremiumCache pendingPremiumCache; + + @Mock + private PremiumService premiumService; + @Mock private Settings settings; @@ -66,7 +78,7 @@ void shouldRegisterIncomingChannelWhenEnabled() { given(settings.getProperty(HooksSettings.BUNGEECORD)).willReturn(true); given(messenger.isIncomingChannelRegistered(plugin, "authme:main")).willReturn(false); - new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, settings); + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, pendingPremiumCache, premiumService, settings); verify(messenger).registerIncomingPluginChannel(eq(plugin), eq("authme:main"), any(BungeeReceiver.class)); } @@ -77,7 +89,7 @@ void shouldUnregisterIncomingChannelWhenDisabledOnReload() { given(messenger.isIncomingChannelRegistered(plugin, "authme:main")).willReturn(false, true); BungeeReceiver bungeeReceiver = - new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, settings); + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, pendingPremiumCache, premiumService, settings); bungeeReceiver.reload(settings); verify(messenger).registerIncomingPluginChannel(plugin, "authme:main", bungeeReceiver); @@ -101,7 +113,7 @@ void shouldQueueSessionAndForceLoginWhenPerformLoginReceivedForOnlinePlayer() { given(bukkitService.getPlayerExact(playerName)).willReturn(player); BungeeReceiver receiver = - new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, settings); + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, pendingPremiumCache, premiumService, settings); byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac); @@ -128,7 +140,7 @@ void shouldOnlyQueueSessionWhenPerformLoginReceivedForOfflinePlayer() { given(bukkitService.getPlayerExact(playerName)).willReturn(null); BungeeReceiver receiver = - new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, settings); + new BungeeReceiver(plugin, bukkitService, proxySessionManager, management, bungeeSender, dataSource, pendingPremiumCache, premiumService, settings); Player carrier = mock(Player.class); byte[] payload = buildPerformLoginPayload(playerName, timestamp, hmac); diff --git a/authme-core/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql b/authme-core/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql index 9af524999..687a1d7d1 100644 --- a/authme-core/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql +++ b/authme-core/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql @@ -19,7 +19,8 @@ CREATE TABLE authme ( isLogged INT DEFAULT '0', realname VARCHAR(255) NOT NULL DEFAULT 'Player', salt varchar(255), - hasSession INT NOT NULL DEFAULT '0' + hasSession INT NOT NULL DEFAULT '0', + premiumUUID VARCHAR(36) ); INSERT INTO authme (username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip, totp) diff --git a/authme-core/src/test/resources/plugin.yml b/authme-core/src/test/resources/plugin.yml index 534d9644a..5d50a4069 100644 --- a/authme-core/src/test/resources/plugin.yml +++ b/authme-core/src/test/resources/plugin.yml @@ -21,7 +21,7 @@ softdepend: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|premium|freemium|debug email: description: Add email or recover password usage: /email show|add|change|recover|code|setpassword @@ -61,6 +61,12 @@ commands: verification: description: Verification command usage: /verification + premium: + description: Enable premium mode + usage: /premium + freemium: + description: Disable premium mode + usage: /freemium permissions: authme.admin.*: description: Gives access to all admin commands @@ -85,6 +91,8 @@ permissions: authme.admin.seeotheraccounts: true authme.admin.seerecent: true authme.admin.setfirstspawn: true + authme.admin.setfreemium: true + authme.admin.setpremium: true authme.admin.setspawn: true authme.admin.spawn: true authme.admin.switchantibot: true @@ -152,6 +160,12 @@ permissions: authme.admin.setfirstspawn: description: Administrator command to set the first AuthMe spawn. default: op + authme.admin.setfreemium: + description: Administrator command to disable premium mode for a player. + default: op + authme.admin.setpremium: + description: Administrator command to enable premium mode for a player. + default: op authme.admin.setspawn: description: Administrator command to set the AuthMe spawn. default: op @@ -252,8 +266,10 @@ permissions: authme.player.email.change: true authme.player.email.recover: true authme.player.email.see: true + authme.player.freemium: true authme.player.login: true authme.player.logout: true + authme.player.premium: true authme.player.protection.quickcommandsprotection: true authme.player.register: true authme.player.security.verificationcode: true @@ -289,12 +305,19 @@ permissions: authme.player.email.see: description: Command permission to see the own email address. default: true + authme.player.freemium: + description: Permission to disable premium mode. + default: true authme.player.login: description: Command permission to login. default: true authme.player.logout: description: Command permission to logout. default: true + authme.player.premium: + description: Permission to enable premium mode (skip authentication using a verified + Mojang account). + default: true authme.player.protection.quickcommandsprotection: description: Permission that enables on join quick commands checks for the player. default: true diff --git a/authme-folia/src/main/resources/plugin.yml b/authme-folia/src/main/resources/plugin.yml index 00918d6e7..f6150e41f 100644 --- a/authme-folia/src/main/resources/plugin.yml +++ b/authme-folia/src/main/resources/plugin.yml @@ -40,7 +40,7 @@ libraries: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|premium|freemium|debug email: description: Add email or recover password usage: /email show|add|change|recover|code|setpassword @@ -80,6 +80,12 @@ commands: verification: description: Verification command usage: /verification + premium: + description: Enable premium mode + usage: /premium + freemium: + description: Disable premium mode + usage: /freemium permissions: authme.admin.*: description: Gives access to all admin commands @@ -104,6 +110,8 @@ permissions: authme.admin.seeotheraccounts: true authme.admin.seerecent: true authme.admin.setfirstspawn: true + authme.admin.setfreemium: true + authme.admin.setpremium: true authme.admin.setspawn: true authme.admin.spawn: true authme.admin.switchantibot: true @@ -171,6 +179,12 @@ permissions: authme.admin.setfirstspawn: description: Administrator command to set the first AuthMe spawn. default: op + authme.admin.setfreemium: + description: Administrator command to disable premium mode for a player. + default: op + authme.admin.setpremium: + description: Administrator command to enable premium mode for a player. + default: op authme.admin.setspawn: description: Administrator command to set the AuthMe spawn. default: op @@ -271,8 +285,10 @@ permissions: authme.player.email.change: true authme.player.email.recover: true authme.player.email.see: true + authme.player.freemium: true authme.player.login: true authme.player.logout: true + authme.player.premium: true authme.player.protection.quickcommandsprotection: true authme.player.register: true authme.player.security.verificationcode: true @@ -308,12 +324,19 @@ permissions: authme.player.email.see: description: Command permission to see the own email address. default: true + authme.player.freemium: + description: Permission to disable premium mode. + default: true authme.player.login: description: Command permission to login. default: true authme.player.logout: description: Command permission to logout. default: true + authme.player.premium: + description: Permission to enable premium mode (skip authentication using a verified + Mojang account). + default: true authme.player.protection.quickcommandsprotection: description: Permission that enables on join quick commands checks for the player. default: true diff --git a/authme-paper-common/pom.xml b/authme-paper-common/pom.xml index adaff04b1..8b0c401de 100644 --- a/authme-paper-common/pom.xml +++ b/authme-paper-common/pom.xml @@ -61,5 +61,10 @@ hamcrest test + + ch.jalu + datasourcecolumns + test + diff --git a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java index 608522ac5..e21dce3c1 100644 --- a/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java +++ b/authme-paper-common/src/main/java/fr/xephi/authme/listener/PaperDialogFlowListener.java @@ -16,8 +16,10 @@ import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.DialogWindowService; import fr.xephi.authme.service.PreJoinDialogService; +import fr.xephi.authme.service.PremiumLoginVerifier; import fr.xephi.authme.service.SessionService; import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.properties.PremiumSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import io.papermc.paper.connection.PlayerConfigurationConnection; @@ -82,6 +84,9 @@ public class PaperDialogFlowListener implements Listener { @Inject private ProxySessionManager proxySessionManager; + @Inject + private PremiumLoginVerifier premiumLoginVerifier; + @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerConfigure(AsyncPlayerConnectionConfigureEvent event) { if (!commonService.getProperty(RegistrationSettings.USE_PREJOIN_DIALOG_UI)) { @@ -111,6 +116,10 @@ public void onPlayerConfigure(AsyncPlayerConnectionConfigureEvent event) { PlayerAuth auth = dataSource.getAuth(normalizedName); if (auth != null) { + if (shouldSkipPreJoinDialogForPremium(auth, playerName, playerId)) { + preJoinDialogService.markSkipPostJoinDialog(playerId); + return; + } handleBlockingLoginDialog(connection, playerId, playerName); } else if (commonService.getProperty(RegistrationSettings.FORCE)) { RegistrationType registrationType = commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE); @@ -296,6 +305,26 @@ private void completeRegisterResponse(UUID playerId, String kickMessage) { } } + private boolean shouldSkipPreJoinDialogForPremium(PlayerAuth auth, String playerName, UUID playerId) { + if (!commonService.getProperty(PremiumSettings.ENABLE_PREMIUM) || !auth.isPremium()) { + return false; + } + if (playerId != null && playerId.version() == 4) { + // UUID v4: Mojang UUID already in the profile (online-mode or proxy forwarded it). + return playerId.equals(auth.getPremiumUuid()); + } + // UUID v3 (offline): check if PacketEvents has already completed verification. + UUID verifiedUuid = premiumLoginVerifier.getVerifiedUuid(playerName); + if (verifiedUuid != null) { + return verifiedUuid.equals(auth.getPremiumUuid()); + } + // UUID is still offline at the configure phase but the player is enrolled as premium. + // Proxy UUID forwarding may not have been applied yet — skip the blocking pre-join dialog + // and let AsynchronousJoin.canBypassWithPremium() do the definitive check once the player + // has fully joined and player.getUniqueId() reflects the proxy-forwarded Mojang UUID. + return true; + } + // MC 1.21.6 (protocol 771) introduced the dialog / custom-click packets required for pre-join dialogs private static final int DIALOG_MIN_PROTOCOL = 771; diff --git a/authme-paper-common/src/main/java/fr/xephi/authme/platform/AbstractPaperPlatformAdapter.java b/authme-paper-common/src/main/java/fr/xephi/authme/platform/AbstractPaperPlatformAdapter.java index daf44f357..50e6300a2 100644 --- a/authme-paper-common/src/main/java/fr/xephi/authme/platform/AbstractPaperPlatformAdapter.java +++ b/authme-paper-common/src/main/java/fr/xephi/authme/platform/AbstractPaperPlatformAdapter.java @@ -74,6 +74,14 @@ public List> getListeners() { return EventRegistrationAdapter.getCommonListeners(); } + @Override + public boolean isProxyForwardingEnabled() { + if (super.isProxyForwardingEnabled()) { + return true; + } + return isPaperVelocityForwardingEnabled(); + } + private static boolean hasDialogApi() { try { Class.forName("io.papermc.paper.dialog.Dialog", false, AbstractPaperPlatformAdapter.class.getClassLoader()); @@ -82,4 +90,20 @@ private static boolean hasDialogApi() { return false; } } + + private static boolean isPaperVelocityForwardingEnabled() { + try { + // io.papermc.paper.configuration.GlobalConfiguration is Paper 1.18+ + Class cfg = Class.forName( + "io.papermc.paper.configuration.GlobalConfiguration", + false, AbstractPaperPlatformAdapter.class.getClassLoader()); + Object instance = cfg.getMethod("get").invoke(null); + Object proxies = cfg.getField("proxies").get(instance); + Object velocity = proxies.getClass().getField("velocity").get(proxies); + Object enabled = velocity.getClass().getField("enabled").get(velocity); + return Boolean.TRUE.equals(enabled); + } catch (Exception ignored) { + return false; + } + } } diff --git a/authme-paper-common/src/test/java/fr/xephi/authme/listener/PaperDialogFlowListenerTest.java b/authme-paper-common/src/test/java/fr/xephi/authme/listener/PaperDialogFlowListenerTest.java index fa2d1bf69..518b33caf 100644 --- a/authme-paper-common/src/test/java/fr/xephi/authme/listener/PaperDialogFlowListenerTest.java +++ b/authme-paper-common/src/test/java/fr/xephi/authme/listener/PaperDialogFlowListenerTest.java @@ -8,9 +8,15 @@ import fr.xephi.authme.platform.PaperDialogActionKeys; import fr.xephi.authme.process.register.RegisterSecondaryArgument; import fr.xephi.authme.process.register.RegistrationType; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.DialogWindowService; import fr.xephi.authme.service.PreJoinDialogService; +import fr.xephi.authme.service.PremiumLoginVerifier; import fr.xephi.authme.service.SessionService; +import fr.xephi.authme.settings.properties.PremiumSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import io.papermc.paper.connection.PlayerConfigurationConnection; @@ -288,6 +294,170 @@ public void shouldSkipPreJoinDialogsForProxyAutoLogin() throws Exception { verifyNoInteractions(sessionService); } + @Test + public void shouldSkipPreJoinDialogsForVerifiedPremiumPlayer() throws Exception { + PaperDialogFlowListener listener = new PaperDialogFlowListener(); + CommonService commonService = mock(CommonService.class); + PlayerCache playerCache = mock(PlayerCache.class); + DataSource dataSource = mock(DataSource.class); + PreJoinDialogService preJoinDialogService = mock(PreJoinDialogService.class); + SessionService sessionService = mock(SessionService.class); + ProxySessionManager proxySessionManager = mock(ProxySessionManager.class); + PremiumLoginVerifier premiumLoginVerifier = mock(PremiumLoginVerifier.class); + setField(listener, "commonService", commonService); + setField(listener, "playerCache", playerCache); + setField(listener, "dataSource", dataSource); + setField(listener, "preJoinDialogService", preJoinDialogService); + setField(listener, "sessionService", sessionService); + setField(listener, "proxySessionManager", proxySessionManager); + setField(listener, "premiumLoginVerifier", premiumLoginVerifier); + + UUID premiumUuid = UUID.randomUUID(); + // Offline (v3) UUID simulates an offline-mode backend without proxy + UUID playerId = UUID.nameUUIDFromBytes("bobby".getBytes()); + PlayerAuth auth = PlayerAuth.builder() + .name("bobby") + .password(new HashedPassword("hash")) + .premiumUuid(premiumUuid) + .build(); + + given(commonService.getProperty(RegistrationSettings.USE_PREJOIN_DIALOG_UI)).willReturn(true); + given(commonService.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)).willReturn(Set.of()); + given(commonService.getProperty(PremiumSettings.ENABLE_PREMIUM)).willReturn(true); + given(playerCache.isAuthenticated("bobby")).willReturn(false); + given(proxySessionManager.shouldResumeSession("bobby")).willReturn(false); + given(sessionService.hasValidSession("bobby", null)).willReturn(false); + given(dataSource.getAuth("bobby")).willReturn(auth); + given(premiumLoginVerifier.getVerifiedUuid("Bobby")).willReturn(premiumUuid); + + PlayerProfile profile = mock(PlayerProfile.class); + given(profile.getId()).willReturn(playerId); + given(profile.getName()).willReturn("Bobby"); + + Audience audience = mock(Audience.class); + PlayerConfigurationConnection connection = mock(PlayerConfigurationConnection.class); + given(connection.getProfile()).willReturn(profile); + given(connection.getAudience()).willReturn(audience); + + AsyncPlayerConnectionConfigureEvent event = mock(AsyncPlayerConnectionConfigureEvent.class); + given(event.getConnection()).willReturn(connection); + + listener.onPlayerConfigure(event); + + verify(preJoinDialogService).markSkipPostJoinDialog(playerId); + verifyNoInteractions(audience); + } + + @Test + public void shouldNotSkipPreJoinDialogsForUnverifiedPremiumPlayer() throws Exception { + PaperDialogFlowListener listener = new PaperDialogFlowListener(); + CommonService commonService = mock(CommonService.class); + PlayerCache playerCache = mock(PlayerCache.class); + DataSource dataSource = mock(DataSource.class); + PreJoinDialogService preJoinDialogService = mock(PreJoinDialogService.class); + SessionService sessionService = mock(SessionService.class); + ProxySessionManager proxySessionManager = mock(ProxySessionManager.class); + PremiumLoginVerifier premiumLoginVerifier = mock(PremiumLoginVerifier.class); + setField(listener, "commonService", commonService); + setField(listener, "playerCache", playerCache); + setField(listener, "dataSource", dataSource); + setField(listener, "preJoinDialogService", preJoinDialogService); + setField(listener, "sessionService", sessionService); + setField(listener, "proxySessionManager", proxySessionManager); + setField(listener, "premiumLoginVerifier", premiumLoginVerifier); + + UUID premiumUuid = UUID.randomUUID(); + UUID playerId = UUID.randomUUID(); + PlayerAuth auth = PlayerAuth.builder() + .name("bobby") + .password(new HashedPassword("hash")) + .premiumUuid(premiumUuid) + .build(); + + given(commonService.getProperty(PremiumSettings.ENABLE_PREMIUM)).willReturn(true); + given(dataSource.getAuth("bobby")).willReturn(auth); + given(premiumLoginVerifier.getVerifiedUuid("Bobby")).willReturn(null); // not yet verified + + // UUID v4 that doesn't match stored premium UUID → must return false (impostor or wrong account) + assertThat(invokeShouldSkipPreJoinDialogForPremium(listener, auth, "Bobby", playerId), is(false)); + verify(preJoinDialogService, never()).markSkipPostJoinDialog(playerId); + } + + @Test + public void shouldSkipPreJoinDialogForPremiumPlayerWithOfflineUuidInProxyMode() throws Exception { + // When the PlayerProfile at the configuration phase still has an offline UUID (v3) because + // proxy forwarding hasn't been applied yet, the pre-join dialog must be skipped and the + // final UUID check deferred to AsynchronousJoin. + PaperDialogFlowListener listener = new PaperDialogFlowListener(); + CommonService commonService = mock(CommonService.class); + PremiumLoginVerifier premiumLoginVerifier = mock(PremiumLoginVerifier.class); + setField(listener, "commonService", commonService); + setField(listener, "premiumLoginVerifier", premiumLoginVerifier); + + UUID premiumUuid = UUID.fromString("12345678-1234-4234-b234-123456789abc"); // v4 + // UUID v3 = offline player UUID (NameBasedGenerator → md5 variant, version 3) + UUID offlineUuid = UUID.fromString("7b6d7e2a-0000-3000-8000-000000000001"); // v3 + PlayerAuth auth = PlayerAuth.builder() + .name("bobby") + .password(new HashedPassword("hash")) + .premiumUuid(premiumUuid) + .build(); + + given(commonService.getProperty(PremiumSettings.ENABLE_PREMIUM)).willReturn(true); + given(premiumLoginVerifier.getVerifiedUuid("Bobby")).willReturn(null); // PacketEvents not active + + assertThat(invokeShouldSkipPreJoinDialogForPremium(listener, auth, "Bobby", offlineUuid), is(true)); + } + + @Test + public void shouldSkipPreJoinDialogForPremiumPlayerWithMatchingMojangUuidInProxyMode() throws Exception { + // When the proxy has already forwarded the Mojang UUID (v4) into the PlayerProfile, + // we can verify directly at the pre-join phase. + PaperDialogFlowListener listener = new PaperDialogFlowListener(); + CommonService commonService = mock(CommonService.class); + setField(listener, "commonService", commonService); + + UUID premiumUuid = UUID.randomUUID(); // v4 random + PlayerAuth auth = PlayerAuth.builder() + .name("bobby") + .password(new HashedPassword("hash")) + .premiumUuid(premiumUuid) + .build(); + + given(commonService.getProperty(PremiumSettings.ENABLE_PREMIUM)).willReturn(true); + + // Profile already has the Mojang UUID (v4) — verify returns true + assertThat(invokeShouldSkipPreJoinDialogForPremium(listener, auth, "Bobby", premiumUuid), is(true)); + } + + @Test + public void shouldNotSkipPreJoinDialogForImpostorWithMismatchedMojangUuidInProxyMode() throws Exception { + // An impostor with a different Mojang UUID (v4) must NOT bypass the dialog. + PaperDialogFlowListener listener = new PaperDialogFlowListener(); + CommonService commonService = mock(CommonService.class); + setField(listener, "commonService", commonService); + + UUID storedPremiumUuid = UUID.randomUUID(); // v4 — the legitimate player's UUID + UUID impostorUuid = UUID.randomUUID(); // v4 — a different Mojang account + PlayerAuth auth = PlayerAuth.builder() + .name("bobby") + .password(new HashedPassword("hash")) + .premiumUuid(storedPremiumUuid) + .build(); + + given(commonService.getProperty(PremiumSettings.ENABLE_PREMIUM)).willReturn(true); + + assertThat(invokeShouldSkipPreJoinDialogForPremium(listener, auth, "Bobby", impostorUuid), is(false)); + } + + private static boolean invokeShouldSkipPreJoinDialogForPremium(PaperDialogFlowListener listener, + PlayerAuth auth, String playerName, UUID playerId) throws ReflectiveOperationException { + var method = PaperDialogFlowListener.class + .getDeclaredMethod("shouldSkipPreJoinDialogForPremium", PlayerAuth.class, String.class, UUID.class); + method.setAccessible(true); + return (boolean) method.invoke(listener, auth, playerName, playerId); + } + private static void setField(Object target, String fieldName, Object value) throws ReflectiveOperationException { Field field = target.getClass().getDeclaredField(fieldName); field.setAccessible(true); diff --git a/authme-paper/src/main/resources/plugin.yml b/authme-paper/src/main/resources/plugin.yml index 94911adf3..fe15609d9 100644 --- a/authme-paper/src/main/resources/plugin.yml +++ b/authme-paper/src/main/resources/plugin.yml @@ -39,7 +39,7 @@ libraries: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|premium|freemium|debug email: description: Add email or recover password usage: /email show|add|change|recover|code|setpassword @@ -79,6 +79,12 @@ commands: verification: description: Verification command usage: /verification + premium: + description: Enable premium mode + usage: /premium + freemium: + description: Disable premium mode + usage: /freemium permissions: authme.admin.*: description: Gives access to all admin commands @@ -103,6 +109,8 @@ permissions: authme.admin.seeotheraccounts: true authme.admin.seerecent: true authme.admin.setfirstspawn: true + authme.admin.setfreemium: true + authme.admin.setpremium: true authme.admin.setspawn: true authme.admin.spawn: true authme.admin.switchantibot: true @@ -170,6 +178,12 @@ permissions: authme.admin.setfirstspawn: description: Administrator command to set the first AuthMe spawn. default: op + authme.admin.setfreemium: + description: Administrator command to disable premium mode for a player. + default: op + authme.admin.setpremium: + description: Administrator command to enable premium mode for a player. + default: op authme.admin.setspawn: description: Administrator command to set the AuthMe spawn. default: op @@ -270,8 +284,10 @@ permissions: authme.player.email.change: true authme.player.email.recover: true authme.player.email.see: true + authme.player.freemium: true authme.player.login: true authme.player.logout: true + authme.player.premium: true authme.player.protection.quickcommandsprotection: true authme.player.register: true authme.player.security.verificationcode: true @@ -307,12 +323,19 @@ permissions: authme.player.email.see: description: Command permission to see the own email address. default: true + authme.player.freemium: + description: Permission to disable premium mode. + default: true authme.player.login: description: Command permission to login. default: true authme.player.logout: description: Command permission to logout. default: true + authme.player.premium: + description: Permission to enable premium mode (skip authentication using a verified + Mojang account). + default: true authme.player.protection.quickcommandsprotection: description: Permission that enables on join quick commands checks for the player. default: true diff --git a/authme-spigot-1.21/src/main/resources/plugin.yml b/authme-spigot-1.21/src/main/resources/plugin.yml index 94911adf3..fe15609d9 100644 --- a/authme-spigot-1.21/src/main/resources/plugin.yml +++ b/authme-spigot-1.21/src/main/resources/plugin.yml @@ -39,7 +39,7 @@ libraries: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|premium|freemium|debug email: description: Add email or recover password usage: /email show|add|change|recover|code|setpassword @@ -79,6 +79,12 @@ commands: verification: description: Verification command usage: /verification + premium: + description: Enable premium mode + usage: /premium + freemium: + description: Disable premium mode + usage: /freemium permissions: authme.admin.*: description: Gives access to all admin commands @@ -103,6 +109,8 @@ permissions: authme.admin.seeotheraccounts: true authme.admin.seerecent: true authme.admin.setfirstspawn: true + authme.admin.setfreemium: true + authme.admin.setpremium: true authme.admin.setspawn: true authme.admin.spawn: true authme.admin.switchantibot: true @@ -170,6 +178,12 @@ permissions: authme.admin.setfirstspawn: description: Administrator command to set the first AuthMe spawn. default: op + authme.admin.setfreemium: + description: Administrator command to disable premium mode for a player. + default: op + authme.admin.setpremium: + description: Administrator command to enable premium mode for a player. + default: op authme.admin.setspawn: description: Administrator command to set the AuthMe spawn. default: op @@ -270,8 +284,10 @@ permissions: authme.player.email.change: true authme.player.email.recover: true authme.player.email.see: true + authme.player.freemium: true authme.player.login: true authme.player.logout: true + authme.player.premium: true authme.player.protection.quickcommandsprotection: true authme.player.register: true authme.player.security.verificationcode: true @@ -307,12 +323,19 @@ permissions: authme.player.email.see: description: Command permission to see the own email address. default: true + authme.player.freemium: + description: Permission to disable premium mode. + default: true authme.player.login: description: Command permission to login. default: true authme.player.logout: description: Command permission to logout. default: true + authme.player.premium: + description: Permission to enable premium mode (skip authentication using a verified + Mojang account). + default: true authme.player.protection.quickcommandsprotection: description: Permission that enables on join quick commands checks for the player. default: true diff --git a/authme-spigot-legacy/src/main/resources/plugin.yml b/authme-spigot-legacy/src/main/resources/plugin.yml index 94911adf3..fe15609d9 100644 --- a/authme-spigot-legacy/src/main/resources/plugin.yml +++ b/authme-spigot-legacy/src/main/resources/plugin.yml @@ -39,7 +39,7 @@ libraries: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|premium|freemium|debug email: description: Add email or recover password usage: /email show|add|change|recover|code|setpassword @@ -79,6 +79,12 @@ commands: verification: description: Verification command usage: /verification + premium: + description: Enable premium mode + usage: /premium + freemium: + description: Disable premium mode + usage: /freemium permissions: authme.admin.*: description: Gives access to all admin commands @@ -103,6 +109,8 @@ permissions: authme.admin.seeotheraccounts: true authme.admin.seerecent: true authme.admin.setfirstspawn: true + authme.admin.setfreemium: true + authme.admin.setpremium: true authme.admin.setspawn: true authme.admin.spawn: true authme.admin.switchantibot: true @@ -170,6 +178,12 @@ permissions: authme.admin.setfirstspawn: description: Administrator command to set the first AuthMe spawn. default: op + authme.admin.setfreemium: + description: Administrator command to disable premium mode for a player. + default: op + authme.admin.setpremium: + description: Administrator command to enable premium mode for a player. + default: op authme.admin.setspawn: description: Administrator command to set the AuthMe spawn. default: op @@ -270,8 +284,10 @@ permissions: authme.player.email.change: true authme.player.email.recover: true authme.player.email.see: true + authme.player.freemium: true authme.player.login: true authme.player.logout: true + authme.player.premium: true authme.player.protection.quickcommandsprotection: true authme.player.register: true authme.player.security.verificationcode: true @@ -307,12 +323,19 @@ permissions: authme.player.email.see: description: Command permission to see the own email address. default: true + authme.player.freemium: + description: Permission to disable premium mode. + default: true authme.player.login: description: Command permission to login. default: true authme.player.logout: description: Command permission to logout. default: true + authme.player.premium: + description: Permission to enable premium mode (skip authentication using a verified + Mojang account). + default: true authme.player.protection.quickcommandsprotection: description: Permission that enables on join quick commands checks for the player. default: true diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java index 6bec0ffb9..1fc06b37f 100644 --- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java +++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/AuthMeVelocityPlugin.java @@ -5,7 +5,10 @@ import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; @@ -29,14 +32,12 @@ public final class AuthMeVelocityPlugin extends AbstractAuthMeVelocityPlugin { private final ProxyServer server; private VelocityConfigManager configManager; - private final Logger logger; private final VelocityProxyBridge proxyBridge; @Inject public AuthMeVelocityPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { super(server, logger, dataDirectory); this.server = server; - this.logger = logger; this.proxyBridge = createProxyBridge(server, logger, dataDirectory); } @@ -81,6 +82,21 @@ public void onPlayerChat(PlayerChatEvent event) { proxyBridge.onPlayerChat(event); } + @Subscribe + public void onPreLogin(PreLoginEvent event) { + proxyBridge.onPreLogin(event); + } + + @Subscribe + public void onLogin(LoginEvent event) { + proxyBridge.onLogin(event); + } + + @Subscribe + public void onPostLogin(PostLoginEvent event) { + proxyBridge.onPostLogin(event); + } + @Subscribe public void onDisconnect(DisconnectEvent event) { proxyBridge.onDisconnect(event); 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 2ba648a78..403b2af3b 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 @@ -5,7 +5,9 @@ import com.google.common.io.ByteStreams; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; @@ -41,6 +43,10 @@ final class VelocityProxyBridge { private static final String PERFORM_LOGIN_MESSAGE = "perform.login"; private static final String PERFORM_LOGIN_ACK_MESSAGE = "perform.login.ack"; private static final String PROXY_STARTED_MESSAGE = "proxy.started"; + private static final String PREMIUM_SET_MESSAGE = "premium.set"; + private static final String PREMIUM_UNSET_MESSAGE = "premium.unset"; + private static final String PREMIUM_LIST_MESSAGE = "premium.list"; + private static final String PREMIUM_PENDING_SET_MESSAGE = "premium.pending.set"; private static final String PROXY_IDENTITY = "velocity"; private static final int MAX_RETRIES = 3; @@ -50,6 +56,11 @@ final class VelocityProxyBridge { private final VelocityAuthenticationStore authenticationStore; private final Map pendingAutoLogins = new ConcurrentHashMap<>(); private final Set notifiedAuthServers = ConcurrentHashMap.newKeySet(); + private volatile Set premiumUsernames = ConcurrentHashMap.newKeySet(); + // Players with a pending premium verification (ran /premium but not yet confirmed via reconnect) + private volatile Set pendingPremiumUsernames = ConcurrentHashMap.newKeySet(); + // Players whose Mojang UUID was confirmed by the proxy during the login phase (LoginSuccess with UUID v4) + private final Set proxyVerifiedPremium = ConcurrentHashMap.newKeySet(); private final ScheduledExecutorService retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "authme-velocity-retry"); t.setDaemon(true); @@ -64,6 +75,39 @@ final class VelocityProxyBridge { this.authenticationStore = authenticationStore; } + private void markProxyVerifiedPremium(String normalizedName) { + if (proxyVerifiedPremium.add(normalizedName)) { + logger.info("Proxy-verified premium: '{}' authenticated online-mode with Mojang", normalizedName); + } + } + + /** + * Records the player as proxy-verified premium when the proxy finished the Mojang authentication + * phase in online mode. Called from {@code AuthMeVelocityPlugin#onLogin(LoginEvent)}. + */ + void onLogin(LoginEvent event) { + Player player = event.getPlayer(); + if (!player.isOnlineMode()) { + return; + } + markProxyVerifiedPremium(normalizeName(player.getUsername())); + } + + /** + * Fallback hook on {@link PostLoginEvent}: if the player has a UUID v4 (Mojang-issued), they + * are recorded as proxy-verified premium even if the {@link LoginEvent} listener missed them. + * + *

    This is a secondary signal only — the primary gate is the backend's UUID comparison + * ({@code verifiedUuid.equals(auth.getPremiumUuid())}), so a fake v4 UUID injected by a + * third-party plugin would still be rejected there unless it matches the stored premium UUID.

    + */ + void onPostLogin(PostLoginEvent event) { + Player player = event.getPlayer(); + if (player.getUniqueId() != null && player.getUniqueId().version() == 4) { + markProxyVerifiedPremium(normalizeName(player.getUsername())); + } + } + void reload(VelocityProxyConfiguration configuration) { this.configuration = configuration; logger.info("Configuration reloaded"); @@ -155,6 +199,7 @@ void onPluginMessage(PluginMessageEvent event) { logger.info("Player {} authenticated on auth server '{}'", parsedMessage.playerName(), serverName); authenticationStore.markAuthenticated(parsedMessage.playerName()); sendAutoLoginIfAlreadySwitched(parsedMessage.playerName(), serverConnection.getServer()); + redirectToLoginServer(parsedMessage.playerName()); } else if (pendingAutoLogins.containsKey(parsedMessage.playerName())) { // Implicit ACK: login from non-auth server confirms perform.login was processed logger.info("Auto-login confirmed for {} via login from server '{}'", @@ -172,6 +217,28 @@ void onPluginMessage(PluginMessageEvent event) { logger.info("Auto-login ACK received for {} from server '{}'", parsedMessage.playerName(), serverName); cancelPendingLogin(parsedMessage.playerName()); + } else if (PREMIUM_SET_MESSAGE.equals(parsedMessage.typeId())) { + premiumUsernames.add(parsedMessage.playerName()); + pendingPremiumUsernames.remove(parsedMessage.playerName()); + logger.debug("Premium enabled for '{}' (proxy cache updated)", parsedMessage.playerName()); + } else if (PREMIUM_UNSET_MESSAGE.equals(parsedMessage.typeId())) { + premiumUsernames.remove(parsedMessage.playerName()); + pendingPremiumUsernames.remove(parsedMessage.playerName()); + logger.debug("Premium disabled for '{}' (proxy cache updated)", parsedMessage.playerName()); + } else if (PREMIUM_PENDING_SET_MESSAGE.equals(parsedMessage.typeId())) { + pendingPremiumUsernames.add(parsedMessage.playerName()); + logger.debug("Pending premium verification started for '{}'", parsedMessage.playerName()); + } else if (PREMIUM_LIST_MESSAGE.equals(parsedMessage.typeId())) { + Set newPremiumSet = ConcurrentHashMap.newKeySet(); + if (!parsedMessage.playerName().isEmpty()) { + for (String name : parsedMessage.playerName().split(",")) { + if (!name.isEmpty()) { + newPremiumSet.add(name.trim()); + } + } + } + premiumUsernames = newPremiumSet; + logger.info("Premium list received from backend: {} premium player(s)", premiumUsernames.size()); } } @@ -202,10 +269,20 @@ void onServerConnected(ServerConnectedEvent event) { String normalizedName = normalizeName(playerName); - if (!authenticationStore.isAuthenticated(normalizedName)) { - logger.debug("Skipping auto-login for {} — not marked as authenticated on the proxy", normalizedName); + // Pending players have passed Mojang auth at the proxy, but we must NOT send PERFORM_LOGIN + // for them: the backend needs to run canBypassWithPremium() to finalize (persist) the premium + // UUID. Only confirmed premium players (premiumUsernames) trigger the auto-login bypass. + boolean isPremiumJoin = connectingToAuthServer + && proxyVerifiedPremium.contains(normalizedName) + && !pendingPremiumUsernames.contains(normalizedName); + if (!authenticationStore.isAuthenticated(normalizedName) && !isPremiumJoin) { + logger.debug("Skipping auto-login for {} — not authenticated or proxy-verified premium", normalizedName); return; } + if (isPremiumJoin) { + logger.debug("Proxy-verified premium player {} joining auth server — sending perform.login immediately", + normalizedName); + } Optional currentServer = event.getPlayer().getCurrentServer(); if (currentServer.isEmpty()) { @@ -290,6 +367,14 @@ void onPlayerChat(PlayerChatEvent event) { event.setResult(PlayerChatEvent.ChatResult.denied()); } + void onPreLogin(com.velocitypowered.api.event.connection.PreLoginEvent event) { + String normalizedName = normalizeName(event.getUsername()); + if (premiumUsernames.contains(normalizedName) || pendingPremiumUsernames.contains(normalizedName)) { + event.setResult(com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult.forceOnlineMode()); + logger.debug("Forcing online-mode for premium player '{}'", normalizedName); + } + } + void onDisconnect(DisconnectEvent event) { String normalizedName = normalizeName(event.getPlayer().getUsername()); if (pendingAutoLogins.containsKey(normalizedName)) { @@ -300,6 +385,8 @@ void onDisconnect(DisconnectEvent event) { logger.debug("Clearing auth state for {} (player disconnected)", normalizedName); } authenticationStore.clear(event.getPlayer()); + proxyVerifiedPremium.remove(normalizedName); + pendingPremiumUsernames.remove(normalizedName); } void shutdown() { @@ -399,11 +486,18 @@ private ParsedMessage parseMessage(byte[] data) { try { String typeId = input.readUTF(); if (!LOGIN_MESSAGE.equals(typeId) && !LOGOUT_MESSAGE.equals(typeId) - && !PERFORM_LOGIN_ACK_MESSAGE.equals(typeId)) { + && !PERFORM_LOGIN_ACK_MESSAGE.equals(typeId) + && !PREMIUM_SET_MESSAGE.equals(typeId) + && !PREMIUM_UNSET_MESSAGE.equals(typeId) + && !PREMIUM_LIST_MESSAGE.equals(typeId) + && !PREMIUM_PENDING_SET_MESSAGE.equals(typeId)) { logger.debug("Ignoring unknown authme:main message type '{}'", typeId); return ParsedMessage.ignored(); } - return new ParsedMessage(typeId, normalizeName(input.readUTF())); + // premium.list carries a CSV in the second field, not a player name; read as-is + String argument = input.readUTF(); + return new ParsedMessage(typeId, + PREMIUM_LIST_MESSAGE.equals(typeId) ? argument : normalizeName(argument)); } catch (IllegalStateException e) { logger.warn("Received malformed AuthMe plugin message on the authme:main channel"); return ParsedMessage.ignored(); @@ -421,6 +515,26 @@ private byte[] createPerformLoginMessage(String normalizedName) { return output.toByteArray(); } + private void redirectToLoginServer(String normalizedPlayerName) { + if (configuration.loginServer().isEmpty()) { + return; + } + Optional playerOpt = proxyServer.getPlayer(normalizedPlayerName); + if (playerOpt.isEmpty()) { + logger.debug("Cannot redirect {} to loginServer: player no longer on proxy", normalizedPlayerName); + return; + } + Optional targetServer = proxyServer.getServer(configuration.loginServer()); + if (targetServer.isEmpty()) { + logger.warn("loginServer '{}' is not registered on the proxy; cannot redirect {}", + configuration.loginServer(), normalizedPlayerName); + return; + } + logger.info("Redirecting {} to login server '{}' after authentication", + normalizedPlayerName, configuration.loginServer()); + playerOpt.get().createConnectionRequest(targetServer.get()).fireAndForget(); + } + private void redirectLoggedOutPlayer(String normalizedPlayerName) { if (!configuration.sendOnLogoutEnabled()) { return; diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyConfiguration.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyConfiguration.java index b732d9a45..ac4ca4d05 100644 --- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyConfiguration.java +++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/VelocityProxyConfiguration.java @@ -23,6 +23,7 @@ final class VelocityProxyConfiguration { private final boolean commandsRequireAuth; private final Set commandWhitelist; private final boolean chatRequiresAuth; + private final String loginServer; private final String sharedSecret; VelocityProxyConfiguration(Set authServers, boolean allServersAreAuthServers, @@ -30,7 +31,7 @@ final class VelocityProxyConfiguration { boolean autoLoginEnabled, boolean sendOnLogoutEnabled, String sendOnLogoutTarget, boolean commandsRequireAuth, Set commandWhitelist, boolean chatRequiresAuth, - String sharedSecret) { + String loginServer, String sharedSecret) { this.authServers = authServers; this.allServersAreAuthServers = allServersAreAuthServers; this.serverSwitchRequiresAuth = serverSwitchRequiresAuth; @@ -41,6 +42,7 @@ final class VelocityProxyConfiguration { this.commandsRequireAuth = commandsRequireAuth; this.commandWhitelist = commandWhitelist; this.chatRequiresAuth = chatRequiresAuth; + this.loginServer = normalizeServerName(loginServer); this.sharedSecret = sharedSecret; } @@ -56,6 +58,7 @@ static VelocityProxyConfiguration from(SettingsManager settingsManager) { settingsManager.getProperty(VelocityConfigProperties.COMMANDS_REQUIRE_AUTH), normalizeCommandAliases(settingsManager.getProperty(VelocityConfigProperties.COMMANDS_WHITELIST)), settingsManager.getProperty(VelocityConfigProperties.CHAT_REQUIRES_AUTH), + settingsManager.getProperty(VelocityConfigProperties.LOGIN_SERVER), settingsManager.getProperty(VelocityConfigProperties.PROXY_SHARED_SECRET)); } @@ -95,6 +98,10 @@ boolean chatRequiresAuth() { return chatRequiresAuth; } + String loginServer() { + return loginServer; + } + String sharedSecret() { return sharedSecret; } diff --git a/authme-velocity/src/main/java/fr/xephi/authme/velocity/config/VelocityConfigProperties.java b/authme-velocity/src/main/java/fr/xephi/authme/velocity/config/VelocityConfigProperties.java index 51ccea41b..6fec8773a 100644 --- a/authme-velocity/src/main/java/fr/xephi/authme/velocity/config/VelocityConfigProperties.java +++ b/authme-velocity/src/main/java/fr/xephi/authme/velocity/config/VelocityConfigProperties.java @@ -52,6 +52,13 @@ public final class VelocityConfigProperties implements SettingsHolder { public static final Property CHAT_REQUIRES_AUTH = newProperty("chatRequiresAuth", true); + @Comment({ + "Server to redirect players to after successful authentication on an auth server.", + "Leave empty to disable proxy-side login redirect (backend handles it via BUNGEECORD_SERVER)." + }) + public static final Property LOGIN_SERVER = + newProperty("loginServer", ""); + @Comment({ "Shared secret used to sign perform.login messages sent to backend servers.", "Generated automatically on first start — copy this value to the proxySharedSecret", 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 097f362f5..1c5c4885f 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 @@ -188,7 +188,7 @@ void shouldRedirectPlayerOnLogoutWhenConfigured() { VelocityProxyBridge bridge = new VelocityProxyBridge( proxyServer, logger, new VelocityProxyConfiguration(Set.of("lobby"), false, true, "Authentication required.", true, true, "limbo", true, - Set.of("/login", "/register"), true, ""), + Set.of("/login", "/register"), true, "", ""), new VelocityAuthenticationStore()); bridge.onPluginMessage(pluginMessageEvent); @@ -432,7 +432,7 @@ void shouldAllowCommandIfSourceIsNotAPlayer() { void shouldNotBlockCommandIfCommandsRequireAuthIsDisabled() { VelocityProxyConfiguration config = new VelocityProxyConfiguration( Set.of("lobby"), false, true, "Authentication required.", false, false, "", - false, Set.of("/login"), true, ""); + false, Set.of("/login"), true, "", ""); VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, config, new VelocityAuthenticationStore()); bridge.onCommandExecute(commandEvent); @@ -504,7 +504,7 @@ void shouldAllowChatIfPlayerHasNoCurrentServer() { void shouldNotBlockChatIfChatRequiresAuthIsDisabled() { VelocityProxyConfiguration config = new VelocityProxyConfiguration( Set.of("lobby"), false, true, "Authentication required.", false, false, "", - true, Set.of("/login"), false, ""); + true, Set.of("/login"), false, "", ""); VelocityProxyBridge bridge = new VelocityProxyBridge(proxyServer, logger, config, new VelocityAuthenticationStore()); bridge.onPlayerChat(chatEvent); @@ -516,7 +516,7 @@ private static VelocityProxyConfiguration createConfiguration() { return new VelocityProxyConfiguration(Set.of("lobby"), false, true, "Authentication required.", true, false, "", true, Set.of("/login", "/register", "/l", "/reg", "/email", "/captcha", "/2fa", "/totp", "/log"), - true, "test-secret"); + true, "", "test-secret"); } private static byte[] createAuthMePayload(String typeId, String playerName) { diff --git a/docs/commands.md b/docs/commands.md index 4aa1fd27f..6cc1c90e6 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,5 +1,5 @@ - + ## AuthMe Commands You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >` @@ -59,6 +59,10 @@ The command tree is shared across the current Spigot Legacy, Spigot 1.21, and Pa
    Requires `authme.admin.updatemessages` - **/authme recent**: Shows the last players that have logged in.
    Requires `authme.admin.seerecent` +- **/authme premium** <player>: Enables premium mode for the specified player. +
    Requires `authme.admin.setpremium` +- **/authme freemium** <player>: Disables premium mode for the specified player. +
    Requires `authme.admin.setfreemium` - **/authme debug** [child] [arg] [arg]: Allows various operations for debugging.
    Requires `authme.debug.command` - **/authme help** [query]: View detailed help for /authme commands. @@ -106,8 +110,14 @@ The command tree is shared across the current Spigot Legacy, Spigot 1.21, and Pa - **/verification** <code>: Command to complete the verification process for AuthMeReloaded.
    Requires `authme.player.security.verificationcode` - **/verification help** [query]: View detailed help for /verification commands. +- **/premium**: Enables premium mode: skip authentication with a verified Mojang account. +
    Requires `authme.player.premium` +- **/premium help** [query]: View detailed help for /premium commands. +- **/freemium**: Disables premium mode and restores password-based authentication. +
    Requires `authme.player.freemium` +- **/freemium help** [query]: View detailed help for /freemium commands. --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Thu Apr 23 19:32:20 CEST 2026 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat May 02 03:44:30 CEST 2026 diff --git a/docs/config.md b/docs/config.md index 8464fd962..b4b273dc8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## AuthMe Configuration The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, @@ -78,6 +78,9 @@ DataSource: mySQLlastlocPitch: pitch # Column for storing players uuids (optional) mySQLPlayerUUID: '' + # Column for storing the Mojang UUID of premium players + # (null if premium mode is off for this player) + mySQLColumnPremiumUUID: premium_uuid # Overrides the size of the DB Connection Pool, default = 10 poolSize: 10 # The maximum lifetime of a connection in the pool, default = 1800 seconds @@ -369,6 +372,15 @@ settings: # Do we need to prevent people to login with another case? # If Xephi is registered, then Xephi can login, but not XEPHI/xephi/XePhI preventOtherCase: true + # Enable premium mode: players with an official Minecraft account + # can skip password authentication. + # Verification method is chosen automatically: + # - online-mode=true: Bukkit already has the Mojang UUID; no PacketEvents needed. + # - offline-mode + proxy: set Hooks.bungeecord=true; UUID is forwarded by proxy. + # - offline-mode, no proxy: PacketEvents required for cryptographic verification. + # Without PacketEvents, premium auto-login is disabled (fail closed). + # Players must use /premium to opt in. + enablePremium: false GroupOptions: # Enables switching a player to defined permission groups before they log in. # See below for a detailed explanation. @@ -621,4 +633,4 @@ To change settings on a running server, save your changes to config.yml and use --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Tue Apr 28 23:00:57 CEST 2026 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat May 02 03:44:30 CEST 2026 diff --git a/docs/permission_nodes.md b/docs/permission_nodes.md index c144035f0..25153e1c6 100644 --- a/docs/permission_nodes.md +++ b/docs/permission_nodes.md @@ -1,5 +1,5 @@ - + ## AuthMe Permission Nodes The following are the permission nodes that are currently supported by the latest dev builds. @@ -28,6 +28,8 @@ and Paper 1.21 builds. - **authme.admin.seeotheraccounts** – Permission to see the other accounts of the players that log in. - **authme.admin.seerecent** – Administrator command to see the last recently logged in players. - **authme.admin.setfirstspawn** – Administrator command to set the first AuthMe spawn. +- **authme.admin.setfreemium** – Administrator command to disable premium mode for a player. +- **authme.admin.setpremium** – Administrator command to enable premium mode for a player. - **authme.admin.setspawn** – Administrator command to set the AuthMe spawn. - **authme.admin.spawn** – Administrator command to teleport to the AuthMe spawn. - **authme.admin.switchantibot** – Administrator command to toggle the AntiBot protection status. @@ -62,8 +64,10 @@ and Paper 1.21 builds. - **authme.player.email.change** – Command permission to change the email address. - **authme.player.email.recover** – Command permission to recover an account using its email address. - **authme.player.email.see** – Command permission to see the own email address. +- **authme.player.freemium** – Permission to disable premium mode. - **authme.player.login** – Command permission to login. - **authme.player.logout** – Command permission to logout. +- **authme.player.premium** – Permission to enable premium mode (skip authentication using a verified Mojang account). - **authme.player.protection.quickcommandsprotection** – Permission that enables on join quick commands checks for the player. - **authme.player.register** – Command permission to register. - **authme.player.security.verificationcode** – Permission to use the email verification codes feature. @@ -76,4 +80,4 @@ and Paper 1.21 builds. --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Thu Apr 23 19:32:21 CEST 2026 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat May 02 03:44:31 CEST 2026 diff --git a/docs/premium.md b/docs/premium.md new file mode 100644 index 000000000..1ab80c7d2 --- /dev/null +++ b/docs/premium.md @@ -0,0 +1,192 @@ +# Premium bypass + +AuthMe can let players with a legitimate Mojang account skip password authentication entirely. +When a premium-enrolled player connects, AuthMe independently verifies their identity by +running a cryptographic handshake with Mojang's session server — no password prompt, no +dialog box. + +## Requirements + +- **PacketEvents** must be installed as a separate plugin on the server, **unless** you + are using proxy mode (see [Behind a proxy](#behind-a-proxy)). + Without PacketEvents in direct-connection mode, premium bypass is disabled at startup + (AuthMe logs a warning and falls back to normal password authentication for everyone). +- For direct connections: an **offline-mode server** that clients reach without a proxy. + +## Setup + +### 1. Install PacketEvents + +Download **PacketEvents 2.x** and drop the jar into your `plugins/` folder. + +### 2. Enable premium mode in `config.yml` + +```yaml +settings: + enablePremium: true +``` + +### 3. Enroll players + +Players must opt in individually after the admin enables premium mode. + +**Player commands:** +```text +/premium — opt in (must be logged in with password first) +/freemium — opt out (reverts to password authentication) +``` +AuthMe fetches the player's Mojang UUID on opt-in and stores it in the database. +From the next login onward the player bypasses the password prompt. + +**Admin commands:** +```text +/authme premium — enrol a player +/authme freemium — remove a player from premium bypass +``` + +--- + +## How it works + +When a premium-enrolled player connects, AuthMe intercepts the Minecraft login handshake at +the packet level: + +``` +Client Server (AuthMe + PacketEvents) Mojang + | | | + |--LOGIN_START(name)-------------->| | + | | ① DB: isPremium → true (async) | + |<--ENCRYPTION_REQUEST-------------| | + | (RSA-1024 public key + | | + | random verify token) | | + | POST /session/minecraft/join ----------------------------------> | + |--ENCRYPTION_RESPONSE------------>| | + | (enc(sharedSecret) + | | + | enc(verifyToken), RSA) | | + | | ② RSA-decrypt sharedSecret | + | | (sync, on event-loop) | + | | ③ Install AES/CFB8 Netty ciphers| + | | (sync — client already sends | + | | encrypted bytes from here) | + | | ④ Verify token + GET /hasJoined | + | | (async) --------------------->| + | |<--- {uuid, name, properties} ---| + | | ⑤ Store verified UUID (60 s TTL)| + | | ⑥ Re-inject LOGIN_START | + | ... player joins PLAY ... | | + | | | + AsynchronousJoin: getVerifiedUuid(name) == auth.getPremiumUuid() → auto-login +``` + +**The cryptographic guarantee:** the server generates a fresh RSA key pair at startup and a +new random verify token per connection. Mojang's `hasJoined` endpoint only returns a 200 +response if the client called `/session/join` with the correct server-id hash derived from +the shared AES key. An attacker who knows only the player's name cannot forge this exchange. + +**Pre-join dialogs (Paper/Folia):** if `settings.registration.usePreJoinDialogUi` is also +enabled, the pre-join dialog is skipped entirely for verified premium players — no blocking +UI shown, no password field displayed. + +--- + +## Behind a proxy + +### Online-mode proxy (Velocity / BungeeCord online-mode) + +When Velocity or BungeeCord runs in **online mode**, the proxy authenticates players with +Mojang before they reach the backend. The proxy then forwards the real Mojang UUID to the +backend via IP forwarding. AuthMe uses that forwarded UUID directly — no PacketEvents +required, no synthetic `ENCRYPTION_REQUEST` sent. + +**Backend configuration:** + +```yaml +settings: + enablePremium: true + +Hooks: + bungeecord: true # trust the UUID forwarded by the proxy +``` + +**Proxy configuration requirements:** + +| Proxy | Required settings | +|---|---| +| Velocity | `player-info-forwarding-mode: MODERN` in `velocity.toml` + shared secret in `paper-global.yml` (or equivalent) | +| BungeeCord | `ip_forward: true` in BungeeCord `config.yml` + `bungeecord: true` in backend `spigot.yml` | + +> **Security:** with `Hooks.bungeecord: true` the backend trusts the UUID forwarded by the proxy. +> The backend port **must** be firewalled so only the proxy can reach it — otherwise +> anyone could connect directly with an arbitrary UUID and bypass authentication. + +PacketEvents is **not** required in this configuration. + +### Offline-mode proxy with AuthMe proxy plugin + +When the proxy runs in **offline mode**, install the matching AuthMe proxy plugin on the proxy: + +- **authme-velocity** for Velocity +- **authme-bungee** for BungeeCord + +These plugins maintain a list of premium-enrolled players and force per-player Mojang +authentication for them via `PreLoginEvent`. The proxy then forwards the verified Mojang UUID +to the backend the same way as in online-mode proxy setup. Set `Hooks.bungeecord: true` on the backend. + +The premium player list is synchronised automatically: +- When the proxy plugin starts, the backend sends the full list of enrolled premium usernames. +- When a player runs `/premium` or `/authme premium `, the backend notifies the proxy + immediately so the cache stays up to date. + +--- + +## Configuration reference + +```yaml +settings: + # Enable premium mode: players with an official Minecraft account + # can skip password authentication. + # Verification method is chosen automatically: + # - online-mode=true: Bukkit already has the Mojang UUID; no PacketEvents needed. + # - offline-mode + proxy: set Hooks.bungeecord=true; UUID is forwarded by proxy. + # - offline-mode, no proxy: PacketEvents required for cryptographic verification. + # Without PacketEvents, premium auto-login is disabled (fail closed). + # Players must use /premium to opt in. + enablePremium: false +``` + +--- + +## Commands + +| Command | Permission | Description | +|---|---|---| +| `/premium` | `authme.player.premium` | Enrol the calling player in premium bypass (must be logged in). | +| `/freemium` | `authme.player.freemium` | Remove the calling player from premium bypass. | +| `/authme premium ` | `authme.admin.setpremium` | Enrol a player (admin). | +| `/authme freemium ` | `authme.admin.setfreemium` | Remove a player from premium bypass (admin). | + +--- + +## Frequently asked questions + +**Q: Can I use this on an online-mode server?** +A: Online-mode servers already enforce Mojang authentication at the server level — you do not +need AuthMe's premium bypass at all. AuthMe is primarily designed for offline-mode servers. + +**Q: What happens if Mojang's session server is down?** +A: The Minecraft client must contact `sessionserver.mojang.com/session/minecraft/join` before +sending `ENCRYPTION_RESPONSE` — if that call fails, the client drops the connection entirely and +the player cannot join at all. On the server side, if `hasJoined` were to return an error anyway, +the verified UUID is not stored and `canBypassWithPremium()` returns `false`, so the feature +always fails closed: connectivity problems never grant unverified access. + +**Q: What happens if a premium player changes their Minecraft username?** +A: The premium bypass is keyed on the **Mojang UUID** (not the name), so a name change does +not invalidate the enrolment. However, since AuthMe accounts are keyed on the lowercase +player name, the player may need to be re-enrolled with `/authme premium` after a rename, +depending on your account-linking configuration. + +**Q: Can a non-premium player impersonate a premium player?** +A: No. The verify-token check and the `hasJoined` call together ensure that only a client +which actually holds the Mojang session for that account can complete the handshake. An +attacker who merely knows the username cannot forge the encrypted shared secret. diff --git a/docs/proxies/bungee/config.yml b/docs/proxies/bungee/config.yml index 39da4a148..fe95db6b8 100644 --- a/docs/proxies/bungee/config.yml +++ b/docs/proxies/bungee/config.yml @@ -1,5 +1,5 @@ # AUTO-GENERATED FILE! Do not edit this directly -# File auto-generated on Fri Apr 24 16:05:06 CEST 2026. See authme-tools/src/test/java/tools/docs/proxies/bungee/config.tpl.yml +# File auto-generated on Sat May 02 03:44:31 CEST 2026. See authme-tools/src/test/java/tools/docs/proxies/bungee/config.tpl.yml # # Reference configuration for the native AuthMe Bungee proxy plugin. # The live file is created in plugins/AuthMeBungee/config.yml. @@ -36,6 +36,9 @@ autoLogin: false sendOnLogout: false # If sendOnLogout is enabled, logged-out users will be sent to this backend unloggedUserServer: '' +# Server to redirect players to after successful authentication on an auth server. +# Leave empty to disable proxy-side login redirect (backend handles it via BUNGEECORD_SERVER). +loginServer: '' # Shared secret used to sign perform.login messages sent to backend servers. # Generated automatically on first start — copy this value to the Hooks.proxySharedSecret # setting of every backend server running AuthMe. diff --git a/docs/proxies/velocity/config.yml b/docs/proxies/velocity/config.yml index 33c7ebe27..28374aa73 100644 --- a/docs/proxies/velocity/config.yml +++ b/docs/proxies/velocity/config.yml @@ -1,5 +1,5 @@ # AUTO-GENERATED FILE! Do not edit this directly -# File auto-generated on Fri Apr 24 16:05:06 CEST 2026. See authme-tools/src/test/java/tools/docs/proxies/velocity/config.tpl.yml +# File auto-generated on Sat May 02 03:44:31 CEST 2026. See authme-tools/src/test/java/tools/docs/proxies/velocity/config.tpl.yml # # Reference configuration for the native AuthMe Velocity proxy plugin. # The live file is created in plugins/authmevelocity/config.yml. @@ -36,6 +36,9 @@ commands: - /log # Block unauthenticated players on auth servers from sending chat messages chatRequiresAuth: true +# Server to redirect players to after successful authentication on an auth server. +# Leave empty to disable proxy-side login redirect (backend handles it via BUNGEECORD_SERVER). +loginServer: '' # Shared secret used to sign perform.login messages sent to backend servers. # Generated automatically on first start — copy this value to the proxySharedSecret # setting of every backend server running AuthMe. diff --git a/docs/translations.md b/docs/translations.md index 840daf78e..68a470707 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -1,5 +1,5 @@ - + # AuthMe Translations The following translations are available in AuthMe. Set `messagesLanguage` to the language code @@ -12,13 +12,13 @@ Code | Language | Translated |   [br](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_br.yml) | Brazilian | 100% | 100 [cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_cz.yml) | Czech | 100% | 100 [de](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_de.yml) | German | 99% | 99 -[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_eo.yml) | Esperanto | 85% | 85 +[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_eo.yml) | Esperanto | 86% | 86 [es](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_es.yml) | Spanish | 100% | 100 [et](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_et.yml) | Estonian | 100% | 100 [eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_eu.yml) | Basque | 100% | 100 -[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_fi.yml) | Finnish | 60% | 60 +[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_fi.yml) | Finnish | 64% | 64 [fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_fr.yml) | French | 100% | 100 -[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_gl.yml) | Galician | 62% | 62 +[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_gl.yml) | Galician | 65% | 65 [hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_hu.yml) | Hungarian | 99% | 99 [id](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_id.yml) | Indonesian | 95% | 95 [it](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_it.yml) | Italian | 100% | 100 @@ -28,20 +28,20 @@ Code | Language | Translated |   [nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_nl.yml) | Dutch | 100% | 100 [pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_pl.yml) | Polish | 100% | 100 [pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_pt.yml) | Portuguese | 100% | 100 -[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_ro.yml) | Romanian | 100% | 100 +[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_ro.yml) | Romanian | 99% | 99 [ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_ru.yml) | Russian | 100% | 100 [si](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_si.yml) | Slovenian | 99% | 99 -[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_sk.yml) | Slovakian | 85% | 85 +[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_sk.yml) | Slovakian | 86% | 86 [sr](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_sr.yml) | Serbian | 99% | 99 [tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_tr.yml) | Turkish | 100% | 100 [uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_uk.yml) | Ukrainian | 100% | 100 [vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_vn.yml) | Vietnamese | 100% | 100 [zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 100% | 100 [zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 99% | 99 -[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 74% | 74 +[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 77% | 77 [zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/authme-core/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 100% | 100 --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Tue Apr 28 23:00:59 CEST 2026 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat May 02 03:44:31 CEST 2026