diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4e0db1..208fc748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ 0.25.0 - RegEx name validation - Add RegEx name validation -- Allow to retrieve the plugin version from LibreLoginProvider \ No newline at end of file +- Allow to retrieve the plugin version from LibreLoginProvider +- Skip the login flow for players already authenticated by Minekube Connect \ No newline at end of file diff --git a/Plugin/src/main/java/xyz/kyngs/librelogin/bungeecord/BungeeCordListener.java b/Plugin/src/main/java/xyz/kyngs/librelogin/bungeecord/BungeeCordListener.java index 2af1e2f2..1fb6e323 100644 --- a/Plugin/src/main/java/xyz/kyngs/librelogin/bungeecord/BungeeCordListener.java +++ b/Plugin/src/main/java/xyz/kyngs/librelogin/bungeecord/BungeeCordListener.java @@ -6,6 +6,7 @@ package xyz.kyngs.librelogin.bungeecord; +import io.netty.channel.Channel; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.PendingConnection; import net.md_5.bungee.api.connection.ProxiedPlayer; @@ -15,6 +16,7 @@ import net.md_5.bungee.event.EventPriority; import xyz.kyngs.librelogin.api.event.exception.EventCancelledException; import xyz.kyngs.librelogin.common.config.ConfigurationKeys; +import xyz.kyngs.librelogin.common.integration.ConnectIntegration; import xyz.kyngs.librelogin.common.listener.AuthenticListeners; import xyz.kyngs.librelogin.common.util.GeneralUtil; @@ -56,6 +58,12 @@ public void onDisconnect(PlayerDisconnectEvent event) { public void onPreLogin(PreLoginEvent event) { if (plugin.fromFloodgate(event.getConnection().getUniqueId())) return; + // Minekube Connect authenticated the player at its own edge, there is no Mojang session left + // for this proxy to verify. Setting online mode would make the proxy send an encryption + // request that can never be answered, so the login flow has to be skipped, just like it is + // for Floodgate players above. + if (fromConnect(event.getConnection())) return; + runAsyncEvent(event, () -> { var result = onPreLogin(event.getConnection().getName(), event.getConnection().getAddress().getAddress()); @@ -93,10 +101,51 @@ private void setField(PendingConnection connection, String fieldName, Object val } } + /** + * Reads the connection channel out of the {@link PendingConnection} implementation, so that the + * attributes other systems put on it can be looked at. BungeeCord exposes neither the field nor + * its type through its API, hence the reflection. + * + * @return the channel, or null if it could not be read + */ + private Channel getChannel(PendingConnection connection) { + try { + Field field = connection.getClass().getDeclaredField("ch"); + field.setAccessible(true); + + Object wrapper = field.get(connection); + if (wrapper == null) return null; + + return (Channel) wrapper.getClass().getMethod("getHandle").invoke(wrapper); + } catch (ReflectiveOperationException | ClassCastException e) { + plugin.getLogger().debug("Failed to read the channel of a pending connection.", e); + return null; + } + } + + /** + * Checks whether the connection was already authenticated by Minekube Connect, which marks such + * connections with the {@code connect-player} channel attribute before any login event fires. + *
+ * This fails open: if the channel cannot be read the connection is treated as if Connect was not + * installed, which is exactly what happened before this check existed. + */ + private boolean fromConnect(PendingConnection connection) { + return ConnectIntegration.isConnectChannel(getChannel(connection)); + } + @EventHandler(priority = EventPriority.LOWEST) public void onProfileRequest(LoginEvent event) { if (plugin.fromFloodgate(event.getConnection().getUniqueId())) return; + if (fromConnect(event.getConnection())) { + // Connect has already put the player's real uuid on the connection, rewriting it here + // would replace it with LibreLogin's own. Remember the uuid, as the channel is no longer + // reachable once the player is online. + plugin.getConnectIntegration().addPlayer(event.getConnection().getUniqueId()); + return; + } + // Note to future self: NEVER EVER RUN THIS ASYNC, IT WILL BREAK PLUGINS var profile = plugin.getDatabaseProvider().getByName(event.getConnection().getName()); diff --git a/Plugin/src/main/java/xyz/kyngs/librelogin/common/AuthenticLibreLogin.java b/Plugin/src/main/java/xyz/kyngs/librelogin/common/AuthenticLibreLogin.java index 5c873072..faa090de 100644 --- a/Plugin/src/main/java/xyz/kyngs/librelogin/common/AuthenticLibreLogin.java +++ b/Plugin/src/main/java/xyz/kyngs/librelogin/common/AuthenticLibreLogin.java @@ -56,6 +56,7 @@ import xyz.kyngs.librelogin.common.database.provider.LibreLoginSQLiteDatabaseProvider; import xyz.kyngs.librelogin.common.event.AuthenticEventProvider; import xyz.kyngs.librelogin.common.image.AuthenticImageProjector; +import xyz.kyngs.librelogin.common.integration.ConnectIntegration; import xyz.kyngs.librelogin.common.integration.FloodgateIntegration; import xyz.kyngs.librelogin.common.integration.luckperms.LuckPermsIntegration; import xyz.kyngs.librelogin.common.listener.LoginTryListener; @@ -98,6 +99,7 @@ public abstract class AuthenticLibreLogin
implements LibreLoginPlugin
cancelOnExit; private final PlatformHandle
platformHandle;
private final Set eventProvider;
@@ -123,6 +125,7 @@ protected AuthenticLibreLogin() {
platformHandle = providePlatformHandle();
forbiddenPasswords = new HashSet<>();
cancelOnExit = HashMultimap.create();
+ connectApi = new ConnectIntegration();
}
public Map
+ * Connect terminates the player's connection at its own edge, performs the Mojang handshake there,
+ * and then relays the player into the proxy over a local channel which it marks with the
+ * {@code connect-player} attribute. There is no second Mojang session left for the proxy to verify,
+ * so forcing online mode on such a connection makes the proxy send an encryption request that can
+ * never be answered and the login never completes.
+ *
+ * Reading the marker needs no compile time dependency on Connect: Netty interns attribute keys by
+ * name, so the attribute is simply absent when Connect is not installed. This is the same approach
+ * the listeners already use for Floodgate's {@code floodgate-player} attribute.
+ *
+ * The channel is only reachable while the player is logging in, so the UUIDs recognized during
+ * login are remembered here and dropped again when the player disconnects. That gives the rest of
+ * the plugin a UUID based check with the same shape as {@link FloodgateIntegration#isFloodgateId}.
+ */
+public class ConnectIntegration {
+
+ private static final AttributeKey> CONNECT_ATTR = AttributeKey.valueOf("connect-player");
+
+ private final Set
+ * Unlike the Floodgate check this one fails open: if the channel cannot be read we simply
+ * proceed as if Connect was not installed, which is exactly what happened before this check
+ * existed.
+ */
+ private boolean fromConnect(InboundConnection connection) {
+ try {
+ return ConnectIntegration.isConnectChannel(getChannel(connection));
+ } catch (Exception e) {
+ plugin.getLogger().debug("Failed to check if player is coming from Connect.", e);
+ return false;
+ }
+ }
+
@Subscribe(order = PostOrder.LAST)
public void onPostLogin(PostLoginEvent event) {
onPostLogin(event.getPlayer(), null);
@@ -94,6 +122,14 @@ public void onProfileRequest(GameProfileRequestEvent event) {
if (existing != null && plugin.fromFloodgate(existing.getId())) return;
+ if (existing != null && fromConnect(event.getConnection())) {
+ // Connect has already put the player's real uuid and skin properties into the profile,
+ // rebuilding it from the original one would throw both away. Remember the uuid, as the
+ // channel is no longer reachable once the player is online.
+ plugin.getConnectIntegration().addPlayer(existing.getId());
+ return;
+ }
+
var profile = plugin.getDatabaseProvider().getByName(event.getUsername());
var gProfile = event.getOriginalProfile();
@@ -109,15 +145,8 @@ public void onPreLogin(PreLoginEvent event) {
// If floodgate is present, attempt to extract the floodgate player from the connection channel.
if (plugin.floodgateEnabled()) {
- Channel channel;
- InboundConnection connection = event.getConnection();
try {
- if (INITIAL_CONNECTION_DELEGATE != null) {
- connection = (InboundConnection) INITIAL_CONNECTION_DELEGATE.get(connection);
- }
-
- Object mcConnection = INITIAL_MINECRAFT_CONNECTION.get(connection);
- channel = (Channel) CHANNEL.get(mcConnection);
+ Channel channel = getChannel(event.getConnection());
if (channel.attr(FLOODGATE_ATTR).get() != null) {
return; // Player is coming from Floodgate
@@ -130,6 +159,14 @@ public void onPreLogin(PreLoginEvent event) {
}
}
+ // Minekube Connect authenticated the player at its own edge, there is no Mojang session
+ // left for this proxy to verify. Forcing online mode would make the proxy send an encryption
+ // request that can never be answered, so the login flow has to be skipped, just like it is
+ // for Floodgate players above.
+ if (fromConnect(event.getConnection())) {
+ return; // Player has already been authenticated by Connect
+ }
+
var result = onPreLogin(event.getUsername(), event.getConnection().getRemoteAddress().getAddress());
event.setResult(