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,10 @@ 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" ;
46+ private static final String PREMIUM_PENDING_SET_MESSAGE = "premium.pending.set" ;
4047 private static final String PROXY_IDENTITY = "bungee" ;
4148 private static final int MAX_RETRIES = 3 ;
4249
@@ -46,6 +53,11 @@ public final class BungeeProxyBridge implements Listener {
4653 private final BungeeAuthenticationStore authenticationStore ;
4754 private final Map <String , AtomicInteger > pendingAutoLogins = new ConcurrentHashMap <>();
4855 private final Set <String > notifiedAuthServers = ConcurrentHashMap .newKeySet ();
56+ private volatile Set <String > premiumUsernames = ConcurrentHashMap .newKeySet ();
57+ // Players with a pending premium verification (ran /premium but not yet confirmed via reconnect)
58+ private volatile Set <String > pendingPremiumUsernames = ConcurrentHashMap .newKeySet ();
59+ // Players whose Mojang UUID was confirmed by the proxy during the login phase (LoginSuccess with UUID v4)
60+ private final Set <String > proxyVerifiedPremium = ConcurrentHashMap .newKeySet ();
4961 private final ScheduledExecutorService retryScheduler = Executors .newSingleThreadScheduledExecutor (r -> {
5062 Thread t = new Thread (r , "authme-bungee-retry" );
5163 t .setDaemon (true );
@@ -60,6 +72,11 @@ public final class BungeeProxyBridge implements Listener {
6072 this .authenticationStore = authenticationStore ;
6173 }
6274
75+ private void markProxyVerifiedPremium (String normalizedName ) {
76+ proxyVerifiedPremium .add (normalizedName );
77+ logger .info ("Proxy-verified premium: '" + normalizedName + "' authenticated online-mode with Mojang" );
78+ }
79+
6380 void reload (BungeeProxyConfiguration configuration ) {
6481 this .configuration = configuration ;
6582 logger .info ("Configuration reloaded" );
@@ -145,6 +162,7 @@ public void onPluginMessage(PluginMessageEvent event) {
145162 + server .getInfo ().getName () + "'" );
146163 authenticationStore .markAuthenticated (parsedMessage .playerName ());
147164 sendAutoLoginIfAlreadySwitched (parsedMessage .playerName (), server .getInfo ());
165+ redirectToLoginServer (parsedMessage .playerName ());
148166 } else if (pendingAutoLogins .containsKey (parsedMessage .playerName ())) {
149167 // Implicit ACK: login from non-auth server confirms perform.login was processed
150168 logger .info ("Auto-login confirmed for " + parsedMessage .playerName ()
@@ -158,6 +176,28 @@ public void onPluginMessage(PluginMessageEvent event) {
158176 logger .info ("Auto-login ACK received for " + parsedMessage .playerName ()
159177 + " from server '" + server .getInfo ().getName () + "'" );
160178 cancelPendingLogin (parsedMessage .playerName ());
179+ } else if (PREMIUM_SET_MESSAGE .equals (parsedMessage .typeId ())) {
180+ premiumUsernames .add (parsedMessage .playerName ());
181+ pendingPremiumUsernames .remove (parsedMessage .playerName ());
182+ logger .fine (() -> "Premium enabled for '" + parsedMessage .playerName () + "' (proxy cache updated)" );
183+ } else if (PREMIUM_UNSET_MESSAGE .equals (parsedMessage .typeId ())) {
184+ premiumUsernames .remove (parsedMessage .playerName ());
185+ pendingPremiumUsernames .remove (parsedMessage .playerName ());
186+ logger .fine (() -> "Premium disabled for '" + parsedMessage .playerName () + "' (proxy cache updated)" );
187+ } else if (PREMIUM_PENDING_SET_MESSAGE .equals (parsedMessage .typeId ())) {
188+ pendingPremiumUsernames .add (parsedMessage .playerName ());
189+ logger .fine (() -> "Pending premium verification started for '" + parsedMessage .playerName () + "'" );
190+ } else if (PREMIUM_LIST_MESSAGE .equals (parsedMessage .typeId ())) {
191+ Set <String > newPremiumSet = ConcurrentHashMap .newKeySet ();
192+ if (!parsedMessage .playerName ().isEmpty ()) {
193+ for (String name : parsedMessage .playerName ().split ("," )) {
194+ if (!name .isEmpty ()) {
195+ newPremiumSet .add (name .trim ());
196+ }
197+ }
198+ }
199+ premiumUsernames = newPremiumSet ;
200+ logger .info ("Premium list received from backend: " + premiumUsernames .size () + " premium player(s)" );
161201 }
162202 }
163203
@@ -173,7 +213,7 @@ public void onServerSwitch(ServerSwitchEvent event) {
173213 return ;
174214 }
175215
176- if (currentServer == null || ! authenticationStore . isAuthenticated ( player ) ) {
216+ if (currentServer == null ) {
177217 return ;
178218 }
179219
@@ -184,6 +224,21 @@ public void onServerSwitch(ServerSwitchEvent event) {
184224 }
185225
186226 String normalizedName = normalizeName (player .getName ());
227+
228+ // Pending players have passed Mojang auth at the proxy, but we must NOT send PERFORM_LOGIN
229+ // for them: the backend needs to run canBypassWithPremium() to finalize (persist) the premium
230+ // UUID. Only confirmed premium players (premiumUsernames) trigger the auto-login bypass.
231+ boolean isPremiumJoin = connectingToAuthServer
232+ && proxyVerifiedPremium .contains (normalizedName )
233+ && !pendingPremiumUsernames .contains (normalizedName );
234+ if (!authenticationStore .isAuthenticated (player ) && !isPremiumJoin ) {
235+ return ;
236+ }
237+ if (isPremiumJoin ) {
238+ logger .fine ("Proxy-verified premium player " + normalizedName
239+ + " joining auth server — sending perform.login immediately" );
240+ }
241+
187242 String serverName = currentServer .getInfo ().getName ();
188243 logger .info ("Sending auto-login request to server '" + serverName + "' for player " + normalizedName );
189244 currentServer .getInfo ().sendData (AUTHME_CHANNEL , createPerformLoginMessage (normalizedName ), false );
@@ -254,6 +309,53 @@ public void onPlayerDisconnect(PlayerDisconnectEvent event) {
254309 }
255310 cancelPendingLogin (normalizedName );
256311 authenticationStore .clear (event .getPlayer ());
312+ proxyVerifiedPremium .remove (normalizedName );
313+ pendingPremiumUsernames .remove (normalizedName );
314+ }
315+
316+ @ EventHandler
317+ public void onPreLogin (PreLoginEvent event ) {
318+ String normalizedName = normalizeName (event .getConnection ().getName ());
319+ if (premiumUsernames .contains (normalizedName ) || pendingPremiumUsernames .contains (normalizedName )) {
320+ event .getConnection ().setOnlineMode (true );
321+ logger .fine ("Forcing online-mode for premium player '" + normalizedName + "'" );
322+ }
323+ }
324+
325+ /**
326+ * Fires after the proxy has finished the Mojang authentication phase for a connecting player.
327+ * If the connection ended up in online mode (real Mojang account verified at the proxy), the
328+ * player is recorded as proxy-verified premium so the auto-login bypass on the auth server
329+ * will fire on {@link ServerSwitchEvent}.
330+ */
331+ @ EventHandler
332+ public void onLogin (LoginEvent event ) {
333+ if (event .isCancelled ()) {
334+ return ;
335+ }
336+ if (!event .getConnection ().isOnlineMode ()) {
337+ return ;
338+ }
339+ String normalizedName = normalizeName (event .getConnection ().getName ());
340+ markProxyVerifiedPremium (normalizedName );
341+ }
342+
343+ /**
344+ * Fallback: if for any reason the {@link LoginEvent} hook did not flag the player (e.g. the
345+ * proxy is in global online mode and {@code isOnlineMode()} on PendingConnection is reported
346+ * after {@code LoginEvent}), {@link PostLoginEvent} still gives us the verified UUID from the
347+ * proxy. A version-4 UUID means Mojang verified the identity.
348+ */
349+ @ EventHandler
350+ public void onPostLogin (PostLoginEvent event ) {
351+ ProxiedPlayer player = event .getPlayer ();
352+ if (player .getUniqueId () != null && player .getUniqueId ().version () == 4 ) {
353+ String normalizedName = normalizeName (player .getName ());
354+ if (proxyVerifiedPremium .add (normalizedName )) {
355+ logger .info ("Proxy-verified premium (PostLogin fallback): '" + normalizedName
356+ + "' has a Mojang UUID" );
357+ }
358+ }
257359 }
258360
259361 void shutdown () {
@@ -350,16 +452,43 @@ private ParsedPluginMessage parsePluginMessage(byte[] data) {
350452 try {
351453 String typeId = input .readUTF ();
352454 if (!LOGIN_MESSAGE .equals (typeId ) && !LOGOUT_MESSAGE .equals (typeId )
353- && !PERFORM_LOGIN_ACK_MESSAGE .equals (typeId )) {
455+ && !PERFORM_LOGIN_ACK_MESSAGE .equals (typeId )
456+ && !PREMIUM_SET_MESSAGE .equals (typeId )
457+ && !PREMIUM_UNSET_MESSAGE .equals (typeId )
458+ && !PREMIUM_LIST_MESSAGE .equals (typeId )
459+ && !PREMIUM_PENDING_SET_MESSAGE .equals (typeId )) {
354460 return ParsedPluginMessage .ignored ();
355461 }
356- return new ParsedPluginMessage (typeId , normalizeName (input .readUTF ()));
462+ // premium.list carries a CSV in the second field, not a player name; read as-is
463+ String argument = input .readUTF ();
464+ return new ParsedPluginMessage (typeId ,
465+ PREMIUM_LIST_MESSAGE .equals (typeId ) ? argument : normalizeName (argument ));
357466 } catch (IllegalStateException e ) {
358467 logger .warning ("Received malformed AuthMe plugin message on the authme:main channel" );
359468 return ParsedPluginMessage .ignored ();
360469 }
361470 }
362471
472+ private void redirectToLoginServer (String normalizedPlayerName ) {
473+ if (configuration .loginServer ().isEmpty ()) {
474+ return ;
475+ }
476+ ProxiedPlayer player = proxyServer .getPlayer (normalizedPlayerName );
477+ if (player == null ) {
478+ logger .fine ("Cannot redirect " + normalizedPlayerName + " to loginServer: player no longer on proxy" );
479+ return ;
480+ }
481+ ServerInfo targetServer = proxyServer .getServerInfo (configuration .loginServer ());
482+ if (targetServer == null ) {
483+ logger .warning ("loginServer '" + configuration .loginServer ()
484+ + "' is not registered on the proxy; cannot redirect " + normalizedPlayerName );
485+ return ;
486+ }
487+ logger .info ("Redirecting " + normalizedPlayerName + " to login server '"
488+ + configuration .loginServer () + "' after authentication" );
489+ player .connect (targetServer );
490+ }
491+
363492 private void redirectLoggedOutPlayer (String normalizedPlayerName ) {
364493 if (!configuration .sendOnLogoutEnabled ()) {
365494 return ;
0 commit comments