1010import net .md_5 .bungee .api .connection .ProxiedPlayer ;
1111import net .md_5 .bungee .api .connection .Server ;
1212import net .md_5 .bungee .api .event .ChatEvent ;
13+ import net .md_5 .bungee .api .event .LoginEvent ;
1314import net .md_5 .bungee .api .event .PlayerDisconnectEvent ;
1415import net .md_5 .bungee .api .event .PluginMessageEvent ;
16+ import net .md_5 .bungee .api .event .PostLoginEvent ;
17+ import net .md_5 .bungee .api .event .PreLoginEvent ;
1518import net .md_5 .bungee .api .event .ServerConnectEvent ;
1619import net .md_5 .bungee .api .event .ServerSwitchEvent ;
1720import net .md_5 .bungee .api .plugin .Listener ;
@@ -37,6 +40,9 @@ public final class BungeeProxyBridge implements Listener {
3740 private static final String PERFORM_LOGIN_MESSAGE = "perform.login" ;
3841 private static final String PERFORM_LOGIN_ACK_MESSAGE = "perform.login.ack" ;
3942 private static final String PROXY_STARTED_MESSAGE = "proxy.started" ;
43+ private static final String PREMIUM_SET_MESSAGE = "premium.set" ;
44+ private static final String PREMIUM_UNSET_MESSAGE = "premium.unset" ;
45+ private static final String PREMIUM_LIST_MESSAGE = "premium.list" ;
4046 private static final String PROXY_IDENTITY = "bungee" ;
4147 private static final int MAX_RETRIES = 3 ;
4248
@@ -46,6 +52,9 @@ public final class BungeeProxyBridge implements Listener {
4652 private final BungeeAuthenticationStore authenticationStore ;
4753 private final Map <String , AtomicInteger > pendingAutoLogins = new ConcurrentHashMap <>();
4854 private final Set <String > notifiedAuthServers = ConcurrentHashMap .newKeySet ();
55+ private final Set <String > premiumUsernames = ConcurrentHashMap .newKeySet ();
56+ // Players whose Mojang UUID was confirmed by the proxy during the login phase (LoginSuccess with UUID v4)
57+ private final Set <String > proxyVerifiedPremium = ConcurrentHashMap .newKeySet ();
4958 private final ScheduledExecutorService retryScheduler = Executors .newSingleThreadScheduledExecutor (r -> {
5059 Thread t = new Thread (r , "authme-bungee-retry" );
5160 t .setDaemon (true );
@@ -60,6 +69,11 @@ public final class BungeeProxyBridge implements Listener {
6069 this .authenticationStore = authenticationStore ;
6170 }
6271
72+ private void markProxyVerifiedPremium (String normalizedName ) {
73+ proxyVerifiedPremium .add (normalizedName );
74+ logger .info ("Proxy-verified premium: '" + normalizedName + "' authenticated online-mode with Mojang" );
75+ }
76+
6377 void reload (BungeeProxyConfiguration configuration ) {
6478 this .configuration = configuration ;
6579 logger .info ("Configuration reloaded" );
@@ -145,6 +159,7 @@ public void onPluginMessage(PluginMessageEvent event) {
145159 + server .getInfo ().getName () + "'" );
146160 authenticationStore .markAuthenticated (parsedMessage .playerName ());
147161 sendAutoLoginIfAlreadySwitched (parsedMessage .playerName (), server .getInfo ());
162+ redirectToLoginServer (parsedMessage .playerName ());
148163 } else if (pendingAutoLogins .containsKey (parsedMessage .playerName ())) {
149164 // Implicit ACK: login from non-auth server confirms perform.login was processed
150165 logger .info ("Auto-login confirmed for " + parsedMessage .playerName ()
@@ -158,6 +173,22 @@ public void onPluginMessage(PluginMessageEvent event) {
158173 logger .info ("Auto-login ACK received for " + parsedMessage .playerName ()
159174 + " from server '" + server .getInfo ().getName () + "'" );
160175 cancelPendingLogin (parsedMessage .playerName ());
176+ } else if (PREMIUM_SET_MESSAGE .equals (parsedMessage .typeId ())) {
177+ premiumUsernames .add (parsedMessage .playerName ());
178+ logger .fine ("Premium enabled for '" + parsedMessage .playerName () + "' (proxy cache updated)" );
179+ } else if (PREMIUM_UNSET_MESSAGE .equals (parsedMessage .typeId ())) {
180+ premiumUsernames .remove (parsedMessage .playerName ());
181+ logger .fine ("Premium disabled for '" + parsedMessage .playerName () + "' (proxy cache updated)" );
182+ } else if (PREMIUM_LIST_MESSAGE .equals (parsedMessage .typeId ())) {
183+ premiumUsernames .clear ();
184+ if (!parsedMessage .playerName ().isEmpty ()) {
185+ for (String name : parsedMessage .playerName ().split ("," )) {
186+ if (!name .isEmpty ()) {
187+ premiumUsernames .add (name .trim ());
188+ }
189+ }
190+ }
191+ logger .info ("Premium list received from backend: " + premiumUsernames .size () + " premium player(s)" );
161192 }
162193 }
163194
@@ -173,7 +204,7 @@ public void onServerSwitch(ServerSwitchEvent event) {
173204 return ;
174205 }
175206
176- if (currentServer == null || ! authenticationStore . isAuthenticated ( player ) ) {
207+ if (currentServer == null ) {
177208 return ;
178209 }
179210
@@ -184,6 +215,16 @@ public void onServerSwitch(ServerSwitchEvent event) {
184215 }
185216
186217 String normalizedName = normalizeName (player .getName ());
218+
219+ boolean isPremiumJoin = connectingToAuthServer && proxyVerifiedPremium .contains (normalizedName );
220+ if (!authenticationStore .isAuthenticated (player ) && !isPremiumJoin ) {
221+ return ;
222+ }
223+ if (isPremiumJoin ) {
224+ logger .fine ("Proxy-verified premium player " + normalizedName
225+ + " joining auth server — sending perform.login immediately" );
226+ }
227+
187228 String serverName = currentServer .getInfo ().getName ();
188229 logger .info ("Sending auto-login request to server '" + serverName + "' for player " + normalizedName );
189230 currentServer .getInfo ().sendData (AUTHME_CHANNEL , createPerformLoginMessage (normalizedName ), false );
@@ -254,6 +295,52 @@ public void onPlayerDisconnect(PlayerDisconnectEvent event) {
254295 }
255296 cancelPendingLogin (normalizedName );
256297 authenticationStore .clear (event .getPlayer ());
298+ proxyVerifiedPremium .remove (normalizedName );
299+ }
300+
301+ @ EventHandler
302+ public void onPreLogin (PreLoginEvent event ) {
303+ String normalizedName = normalizeName (event .getConnection ().getName ());
304+ if (premiumUsernames .contains (normalizedName )) {
305+ event .getConnection ().setOnlineMode (true );
306+ logger .fine ("Forcing online-mode for premium player '" + normalizedName + "'" );
307+ }
308+ }
309+
310+ /**
311+ * Fires after the proxy has finished the Mojang authentication phase for a connecting player.
312+ * If the connection ended up in online mode (real Mojang account verified at the proxy), the
313+ * player is recorded as proxy-verified premium so the auto-login bypass on the auth server
314+ * will fire on {@link ServerSwitchEvent}.
315+ */
316+ @ EventHandler
317+ public void onLogin (LoginEvent event ) {
318+ if (event .isCancelled ()) {
319+ return ;
320+ }
321+ if (!event .getConnection ().isOnlineMode ()) {
322+ return ;
323+ }
324+ String normalizedName = normalizeName (event .getConnection ().getName ());
325+ markProxyVerifiedPremium (normalizedName );
326+ }
327+
328+ /**
329+ * Fallback: if for any reason the {@link LoginEvent} hook did not flag the player (e.g. the
330+ * proxy is in global online mode and {@code isOnlineMode()} on PendingConnection is reported
331+ * after {@code LoginEvent}), {@link PostLoginEvent} still gives us the verified UUID from the
332+ * proxy. A version-4 UUID means Mojang verified the identity.
333+ */
334+ @ EventHandler
335+ public void onPostLogin (PostLoginEvent event ) {
336+ ProxiedPlayer player = event .getPlayer ();
337+ if (player .getUniqueId () != null && player .getUniqueId ().version () == 4 ) {
338+ String normalizedName = normalizeName (player .getName ());
339+ if (proxyVerifiedPremium .add (normalizedName )) {
340+ logger .info ("Proxy-verified premium (PostLogin fallback): '" + normalizedName
341+ + "' has a Mojang UUID" );
342+ }
343+ }
257344 }
258345
259346 void shutdown () {
@@ -350,16 +437,42 @@ private ParsedPluginMessage parsePluginMessage(byte[] data) {
350437 try {
351438 String typeId = input .readUTF ();
352439 if (!LOGIN_MESSAGE .equals (typeId ) && !LOGOUT_MESSAGE .equals (typeId )
353- && !PERFORM_LOGIN_ACK_MESSAGE .equals (typeId )) {
440+ && !PERFORM_LOGIN_ACK_MESSAGE .equals (typeId )
441+ && !PREMIUM_SET_MESSAGE .equals (typeId )
442+ && !PREMIUM_UNSET_MESSAGE .equals (typeId )
443+ && !PREMIUM_LIST_MESSAGE .equals (typeId )) {
354444 return ParsedPluginMessage .ignored ();
355445 }
356- return new ParsedPluginMessage (typeId , normalizeName (input .readUTF ()));
446+ // premium.list carries a CSV in the second field, not a player name; read as-is
447+ String argument = input .readUTF ();
448+ return new ParsedPluginMessage (typeId ,
449+ PREMIUM_LIST_MESSAGE .equals (typeId ) ? argument : normalizeName (argument ));
357450 } catch (IllegalStateException e ) {
358451 logger .warning ("Received malformed AuthMe plugin message on the authme:main channel" );
359452 return ParsedPluginMessage .ignored ();
360453 }
361454 }
362455
456+ private void redirectToLoginServer (String normalizedPlayerName ) {
457+ if (configuration .loginServer ().isEmpty ()) {
458+ return ;
459+ }
460+ ProxiedPlayer player = proxyServer .getPlayer (normalizedPlayerName );
461+ if (player == null ) {
462+ logger .fine ("Cannot redirect " + normalizedPlayerName + " to loginServer: player no longer on proxy" );
463+ return ;
464+ }
465+ ServerInfo targetServer = proxyServer .getServerInfo (configuration .loginServer ());
466+ if (targetServer == null ) {
467+ logger .warning ("loginServer '" + configuration .loginServer ()
468+ + "' is not registered on the proxy; cannot redirect " + normalizedPlayerName );
469+ return ;
470+ }
471+ logger .info ("Redirecting " + normalizedPlayerName + " to login server '"
472+ + configuration .loginServer () + "' after authentication" );
473+ player .connect (targetServer );
474+ }
475+
363476 private void redirectLoggedOutPlayer (String normalizedPlayerName ) {
364477 if (!configuration .sendOnLogoutEnabled ()) {
365478 return ;
0 commit comments