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 ;
1821import net .md_5 .bungee .event .EventHandler ;
1922import net .md_5 .bungee .event .EventPriority ;
2023
24+ import java .util .ArrayList ;
25+ import java .util .List ;
2126import java .util .Locale ;
2227import java .util .Map ;
2328import java .util .Set ;
@@ -37,6 +42,11 @@ public final class BungeeProxyBridge implements Listener {
3742 private static final String PERFORM_LOGIN_MESSAGE = "perform.login" ;
3843 private static final String PERFORM_LOGIN_ACK_MESSAGE = "perform.login.ack" ;
3944 private static final String PROXY_STARTED_MESSAGE = "proxy.started" ;
45+ private static final String PREMIUM_SET_MESSAGE = "premium.set" ;
46+ private static final String PREMIUM_UNSET_MESSAGE = "premium.unset" ;
47+ private static final String PREMIUM_LIST_MESSAGE = "premium.list" ;
48+ private static final String PREMIUM_LIST_CHUNK_MESSAGE = "premium.list.chunk" ;
49+ private static final String PREMIUM_PENDING_SET_MESSAGE = "premium.pending.set" ;
4050 private static final String PROXY_IDENTITY = "bungee" ;
4151 private static final int MAX_RETRIES = 3 ;
4252
@@ -46,6 +56,12 @@ public final class BungeeProxyBridge implements Listener {
4656 private final BungeeAuthenticationStore authenticationStore ;
4757 private final Map <String , AtomicInteger > pendingAutoLogins = new ConcurrentHashMap <>();
4858 private final Set <String > notifiedAuthServers = ConcurrentHashMap .newKeySet ();
59+ private volatile Set <String > premiumUsernames = ConcurrentHashMap .newKeySet ();
60+ private List <String > premiumListBuffer = new ArrayList <>();
61+ // Players with a pending premium verification (ran /premium but not yet confirmed via reconnect)
62+ private volatile Set <String > pendingPremiumUsernames = ConcurrentHashMap .newKeySet ();
63+ // Players whose Mojang UUID was confirmed by the proxy during the login phase (LoginSuccess with UUID v4)
64+ private final Set <String > proxyVerifiedPremium = ConcurrentHashMap .newKeySet ();
4965 private final ScheduledExecutorService retryScheduler = Executors .newSingleThreadScheduledExecutor (r -> {
5066 Thread t = new Thread (r , "authme-bungee-retry" );
5167 t .setDaemon (true );
@@ -60,6 +76,11 @@ public final class BungeeProxyBridge implements Listener {
6076 this .authenticationStore = authenticationStore ;
6177 }
6278
79+ private void markProxyVerifiedPremium (String normalizedName ) {
80+ proxyVerifiedPremium .add (normalizedName );
81+ logger .info ("Proxy-verified premium: '" + normalizedName + "' authenticated online-mode with Mojang" );
82+ }
83+
6384 void reload (BungeeProxyConfiguration configuration ) {
6485 this .configuration = configuration ;
6586 logger .info ("Configuration reloaded" );
@@ -145,6 +166,7 @@ public void onPluginMessage(PluginMessageEvent event) {
145166 + server .getInfo ().getName () + "'" );
146167 authenticationStore .markAuthenticated (parsedMessage .playerName ());
147168 sendAutoLoginIfAlreadySwitched (parsedMessage .playerName (), server .getInfo ());
169+ redirectToLoginServer (parsedMessage .playerName ());
148170 } else if (pendingAutoLogins .containsKey (parsedMessage .playerName ())) {
149171 // Implicit ACK: login from non-auth server confirms perform.login was processed
150172 logger .info ("Auto-login confirmed for " + parsedMessage .playerName ()
@@ -158,6 +180,52 @@ public void onPluginMessage(PluginMessageEvent event) {
158180 logger .info ("Auto-login ACK received for " + parsedMessage .playerName ()
159181 + " from server '" + server .getInfo ().getName () + "'" );
160182 cancelPendingLogin (parsedMessage .playerName ());
183+ } else if (PREMIUM_SET_MESSAGE .equals (parsedMessage .typeId ())) {
184+ premiumUsernames .add (parsedMessage .playerName ());
185+ pendingPremiumUsernames .remove (parsedMessage .playerName ());
186+ logger .fine (() -> "Premium enabled for '" + parsedMessage .playerName () + "' (proxy cache updated)" );
187+ } else if (PREMIUM_UNSET_MESSAGE .equals (parsedMessage .typeId ())) {
188+ premiumUsernames .remove (parsedMessage .playerName ());
189+ pendingPremiumUsernames .remove (parsedMessage .playerName ());
190+ logger .fine (() -> "Premium disabled for '" + parsedMessage .playerName () + "' (proxy cache updated)" );
191+ } else if (PREMIUM_PENDING_SET_MESSAGE .equals (parsedMessage .typeId ())) {
192+ pendingPremiumUsernames .add (parsedMessage .playerName ());
193+ logger .fine (() -> "Pending premium verification started for '" + parsedMessage .playerName () + "'" );
194+ } else if (PREMIUM_LIST_MESSAGE .equals (parsedMessage .typeId ())) {
195+ Set <String > newPremiumSet = ConcurrentHashMap .newKeySet ();
196+ if (!parsedMessage .playerName ().isEmpty ()) {
197+ for (String name : parsedMessage .playerName ().split ("," )) {
198+ if (!name .isEmpty ()) {
199+ newPremiumSet .add (name .trim ());
200+ }
201+ }
202+ }
203+ premiumUsernames = newPremiumSet ;
204+ logger .info ("Premium list received from backend: " + premiumUsernames .size () + " premium player(s)" );
205+ } else if (PREMIUM_LIST_CHUNK_MESSAGE .equals (parsedMessage .typeId ())) {
206+ String [] parts = parsedMessage .playerName ().split (":" , 3 );
207+ if (parts .length < 3 ) {
208+ logger .warning ("Malformed premium.list.chunk payload: " + parsedMessage .playerName ());
209+ return ;
210+ }
211+ if ("0" .equals (parts [0 ])) {
212+ premiumListBuffer = new ArrayList <>();
213+ }
214+ String csv = parts [2 ];
215+ if (!csv .isEmpty ()) {
216+ for (String name : csv .split ("," )) {
217+ if (!name .isEmpty ()) {
218+ premiumListBuffer .add (name .trim ());
219+ }
220+ }
221+ }
222+ if ("1" .equals (parts [1 ])) {
223+ Set <String > newPremiumSet = ConcurrentHashMap .newKeySet ();
224+ newPremiumSet .addAll (premiumListBuffer );
225+ premiumUsernames = newPremiumSet ;
226+ premiumListBuffer = new ArrayList <>();
227+ logger .info ("Premium list received from backend: " + premiumUsernames .size () + " premium player(s)" );
228+ }
161229 }
162230 }
163231
@@ -173,7 +241,7 @@ public void onServerSwitch(ServerSwitchEvent event) {
173241 return ;
174242 }
175243
176- if (currentServer == null || ! authenticationStore . isAuthenticated ( player ) ) {
244+ if (currentServer == null ) {
177245 return ;
178246 }
179247
@@ -184,6 +252,21 @@ public void onServerSwitch(ServerSwitchEvent event) {
184252 }
185253
186254 String normalizedName = normalizeName (player .getName ());
255+
256+ // Pending players have passed Mojang auth at the proxy, but we must NOT send PERFORM_LOGIN
257+ // for them: the backend needs to run canBypassWithPremium() to finalize (persist) the premium
258+ // UUID. Only confirmed premium players (premiumUsernames) trigger the auto-login bypass.
259+ boolean isPremiumJoin = connectingToAuthServer
260+ && proxyVerifiedPremium .contains (normalizedName )
261+ && !pendingPremiumUsernames .contains (normalizedName );
262+ if (!authenticationStore .isAuthenticated (player ) && !isPremiumJoin ) {
263+ return ;
264+ }
265+ if (isPremiumJoin ) {
266+ logger .fine ("Proxy-verified premium player " + normalizedName
267+ + " joining auth server — sending perform.login immediately" );
268+ }
269+
187270 String serverName = currentServer .getInfo ().getName ();
188271 logger .info ("Sending auto-login request to server '" + serverName + "' for player " + normalizedName );
189272 currentServer .getInfo ().sendData (AUTHME_CHANNEL , createPerformLoginMessage (normalizedName ), false );
@@ -254,6 +337,53 @@ public void onPlayerDisconnect(PlayerDisconnectEvent event) {
254337 }
255338 cancelPendingLogin (normalizedName );
256339 authenticationStore .clear (event .getPlayer ());
340+ proxyVerifiedPremium .remove (normalizedName );
341+ pendingPremiumUsernames .remove (normalizedName );
342+ }
343+
344+ @ EventHandler
345+ public void onPreLogin (PreLoginEvent event ) {
346+ String normalizedName = normalizeName (event .getConnection ().getName ());
347+ if (premiumUsernames .contains (normalizedName ) || pendingPremiumUsernames .contains (normalizedName )) {
348+ event .getConnection ().setOnlineMode (true );
349+ logger .fine ("Forcing online-mode for premium player '" + normalizedName + "'" );
350+ }
351+ }
352+
353+ /**
354+ * Fires after the proxy has finished the Mojang authentication phase for a connecting player.
355+ * If the connection ended up in online mode (real Mojang account verified at the proxy), the
356+ * player is recorded as proxy-verified premium so the auto-login bypass on the auth server
357+ * will fire on {@link ServerSwitchEvent}.
358+ */
359+ @ EventHandler
360+ public void onLogin (LoginEvent event ) {
361+ if (event .isCancelled ()) {
362+ return ;
363+ }
364+ if (!event .getConnection ().isOnlineMode ()) {
365+ return ;
366+ }
367+ String normalizedName = normalizeName (event .getConnection ().getName ());
368+ markProxyVerifiedPremium (normalizedName );
369+ }
370+
371+ /**
372+ * Fallback: if for any reason the {@link LoginEvent} hook did not flag the player (e.g. the
373+ * proxy is in global online mode and {@code isOnlineMode()} on PendingConnection is reported
374+ * after {@code LoginEvent}), {@link PostLoginEvent} still gives us the verified UUID from the
375+ * proxy. A version-4 UUID means Mojang verified the identity.
376+ */
377+ @ EventHandler
378+ public void onPostLogin (PostLoginEvent event ) {
379+ ProxiedPlayer player = event .getPlayer ();
380+ if (player .getUniqueId () != null && player .getUniqueId ().version () == 4 ) {
381+ String normalizedName = normalizeName (player .getName ());
382+ if (proxyVerifiedPremium .add (normalizedName )) {
383+ logger .info ("Proxy-verified premium (PostLogin fallback): '" + normalizedName
384+ + "' has a Mojang UUID" );
385+ }
386+ }
257387 }
258388
259389 void shutdown () {
@@ -350,16 +480,45 @@ private ParsedPluginMessage parsePluginMessage(byte[] data) {
350480 try {
351481 String typeId = input .readUTF ();
352482 if (!LOGIN_MESSAGE .equals (typeId ) && !LOGOUT_MESSAGE .equals (typeId )
353- && !PERFORM_LOGIN_ACK_MESSAGE .equals (typeId )) {
483+ && !PERFORM_LOGIN_ACK_MESSAGE .equals (typeId )
484+ && !PREMIUM_SET_MESSAGE .equals (typeId )
485+ && !PREMIUM_UNSET_MESSAGE .equals (typeId )
486+ && !PREMIUM_LIST_MESSAGE .equals (typeId )
487+ && !PREMIUM_LIST_CHUNK_MESSAGE .equals (typeId )
488+ && !PREMIUM_PENDING_SET_MESSAGE .equals (typeId )) {
354489 return ParsedPluginMessage .ignored ();
355490 }
356- return new ParsedPluginMessage (typeId , normalizeName (input .readUTF ()));
491+ // premium.list and premium.list.chunk carry non-player-name data; read as-is
492+ String argument = input .readUTF ();
493+ return new ParsedPluginMessage (typeId ,
494+ (PREMIUM_LIST_MESSAGE .equals (typeId ) || PREMIUM_LIST_CHUNK_MESSAGE .equals (typeId ))
495+ ? argument : normalizeName (argument ));
357496 } catch (IllegalStateException e ) {
358497 logger .warning ("Received malformed AuthMe plugin message on the authme:main channel" );
359498 return ParsedPluginMessage .ignored ();
360499 }
361500 }
362501
502+ private void redirectToLoginServer (String normalizedPlayerName ) {
503+ if (configuration .loginServer ().isEmpty ()) {
504+ return ;
505+ }
506+ ProxiedPlayer player = proxyServer .getPlayer (normalizedPlayerName );
507+ if (player == null ) {
508+ logger .fine ("Cannot redirect " + normalizedPlayerName + " to loginServer: player no longer on proxy" );
509+ return ;
510+ }
511+ ServerInfo targetServer = proxyServer .getServerInfo (configuration .loginServer ());
512+ if (targetServer == null ) {
513+ logger .warning ("loginServer '" + configuration .loginServer ()
514+ + "' is not registered on the proxy; cannot redirect " + normalizedPlayerName );
515+ return ;
516+ }
517+ logger .info ("Redirecting " + normalizedPlayerName + " to login server '"
518+ + configuration .loginServer () + "' after authentication" );
519+ player .connect (targetServer );
520+ }
521+
363522 private void redirectLoggedOutPlayer (String normalizedPlayerName ) {
364523 if (!configuration .sendOnLogoutEnabled ()) {
365524 return ;
0 commit comments